Предыстория


Одним прекрасным днем я решил начать изучать английский язык, и непосредственно перед изучением посоветовался с преподавателем, который этот язык преподает, т.к. думал, что он поделиться со мной приобретенным когда-то опытом. В итоге, мы сошлись с ним на том, что мне лучше всего начать свое изучение со слов языка, потому что в основном я использую свои знания, когда посещаю англоязычные сайты, а уж потом браться за грамматику. Он предложил очень интересный способ изучения(по крайней мере для меня). В свое время, чтобы слова отложились в памяти, он брал обычный лист бумаги, разлиновывал его на равные прямоугольники с одной стороны и разрезал лист, затем на одной из сторон получившегося прямоугольника писал английское слово, а на другой перевод, после этого он брал коробку, складывал туда получившиеся карточки, перемешивал и извлекал по одной, смотрел на слово и проговаривал перевод.
Если проделывать данную процедуру каждый день по час или два и периодически, пополнять свою картотеку, то слова запоминаются очень хорошо.

С тех пор я решил, что всегда буду так делать и в ближайшем будущем начну читать на английском языке, как на своем родном! Но через некоторое время меня начало утомлять резать карточки и тратить на это время, к тому же времени мне всегда категорически не хватает, в этот момент меня посетила мысль, а почему бы не создать программу под ОС android(т.к. у меня телефон именно с этой системой), которая будет выполнять аналогичную функцию!

Именно с этого момента зародился проект, которому я не придумал название.

Цель


Целью данного проекта, изначально, являлось автоматизация добавления слов в картотеку и дальнейшая их эксплуатация, но позже меня посетило еще пару идей и было очень интересно развивать проект, но спустя недели две это приелось и мне стало скучно, а полностью его забрасывать, как все предыдущие, я не хотел и поэтому решил попробовать себя в качестве автора данной статьи и поделиться полученным опытом в области сетевого программирования и программирования под android. В данной статье я постараюсь, на сколько это возможно, последовательно и шаг за шагом описать всю процедуру создания данной программы.

Программа


Начнем с того, что я подробно опишу созданный мной продукт и рассмотрю функционал, затем, кого это заинтересует, сможет дальше прочитать, о сотворении данного чуда.

Описание


Первое, о чем хотелось бы поведать, так это о меню, я постарался сделать его как можно привлекательнее, у меня всегда был «отменный» вкус, поэтому вот что из этого вышло.

image

Как видно на изображении, особенного ничего в нем нет, но при нажатии на кнопку «Проверка слов» или «Слова», появляется, так сказать, субменю.

image

image

Как видно из изображений сверху таким же свойством обладают кнопки «В Алфавитном порядке», «В Обратном порядке» и «Рандом». Их все я пометил специальным символом «+», а при раскрытии символ заменяется на «-». При повторном нажатии на один из пунктов раскрытого меню, оно сворачивается в исходное положение.

Перед тем как проходить тест или изменять и удалять слова, нужно их добавить, следовательно сейчас мы рассмотрим именно этот механизм, но перед этим нужно немного отступиться от данной темы, чтобы в дальнейшем все было понятно.

Как я уже говорил, перед созданием проекта, меня посетило много идей и одна из них, это упрощение добавление слов в программу. Никому не секрет, что человек существо ленивое и не каждому придется по вкусу вручную создавать свою картотеку, тем более если нужно заполнить аж три поля(хотя транскрипцию заполнять не обязательно), в век развития интернета и передачи данных на большие расстояния за одно мгновенье.

image

Мне пришла в голову идея, а почему бы пользователю программы не делиться своей картотекой со всем миром? Поэтому, в момент добавления слова в свой личный словарь, пользователь того не осознавая(если есть подключение к интернету) загружает его на сервер, где оно храниться в базе данных и ждет, пока другой пользователь добавит его к себе, а так, сам по себе механизм добавления слов не представляет ничего сложного, прост и понятен даже ребенку.

Также в программе есть функции изменения и удаления слов. Здесь совсем все просто: после нажатия на нужный пункт меню перед нами открывается активити, где слова расположены в алфавитном порядке. Окна удаления и изменения слов практически ничем не отличаются, лишь только иконкой на кнопке со словом, поэтому для их создания применялся один xml-файл.

image

image

Если пользователь изменят слово, то для его изменения используется xml-файл добавления слов, только с другим функционалом, все три поля заполнены старыми значениями.

image

После ввода новых значений, при нажатии на соответствующую кнопку, слово изменяется и сохраняется на устройстве пользователя без внесения изменений в БД на сервере. Удаление слов/слова, сделано тоже очень просто, я даже не представляю себе, как можно усложнить данный процесс.

image

image

Как видно из изображений, можно удалить все слова сразу или же можно удалять каждое слово по одиночке, но перед каждым удалением будет всплывать диалоговое окно с подтверждением действия.

Теперь, когда я описал один из основных функционалов, а именно добавление/удаление/изменение слов, рассмотрим наиболее важный, прохождение теста по данным словам, а затем опишу альтернативный способ добавления слов.

Можно было заметить, на рисунке выше, что тест можно проходить в трех разных порядках: алфавитный, обратный и рандом. Тестируемым объектом может быть либо английский вариант слова, либо русский, либо транскрипция.

image

Как видно из рисунка, активно поле с английским вариантом слова, значит тестируется именно этот вариант. В зависимости от выбранного теста, всегда активно только одно поле ввода, остальные изменению не подлежат. Строка «Статус» указывает на то, верен ли введенный вариант пользователя, для того чтобы это узнать, необходимо нажать на кнопку «Проверка», так же показано сколько слов пройдено и их число, кнопку «Далее» подробно можно не рассматривать, но хотелось бы отметить, что можно было бы добавить кнопку возращения к предыдущему слову. Тест можно завершить дойдя до последнего слова или же пунктом меню.

image

После того как вы прошли или завершили тест, вашему вниманию предоставляется ваш результат.

image

Слова, которые были введены правильно отображаются зеленым цветом, остальные красным, далее вы можете либо перейти в главное меню или пройти тест заново.

Теперь рассмотрим альтернативный способ добавления слов, чтобы им воспользоваться, нужно перейти в пункт меню «Загрузить» и там можно будет просматривать слова, которые были добавлены на сервер, способом описанным выше.

image

На рисунке видно, что данное активити практически не отличается от активити изменения и удаления, за исключением того, что здесь находятся CheckBox'ы, вместо Button'ов.

Хотелось бы отметить, что слова с сервера поступают небольшими порциями, а именно по сто штук, после того как пользователь доходит до предпоследнего, подгружается еще сто и так далее, пока сервер не вернет последнее слово. Для того чтобы слово/слова можно было добавить к себе в картотеку, следует воспользоваться пунктами меню.

image

Как видно на изображении, можно загрузить выделенные слова, или те слова которые загруженные на данный момент. Какое количество слов будет сохранено, показано слева от пункта меню.

На изображениях выше, можно заметить, что у активити изменения/удаления/загрузки слов есть Edit — «Найти», т.к. он реализован одинаков везде, то рассмотрим только на примере окна Загрузки.

Не смотря на простоту реализации, поиск слов в этой программе является моей любимой разработкой(не считай подгрузки слов с сервера). Он является «живим», ищет, английские и русские слова, а также прост и удобен в применении.

image

image

image

image

Так же в программе имеется пункт меню «Просмотреть слова», где все слова выводятся в HTML-таблице.

image

Напоследок хотелось бы отметить, что весь функционал всех активити, продублирован в меню.

image

image

image

image

image

Реализация


Как было не трудно догадаться, программа состоит из двух частей, это серверное и клиентское приложение, так что данный пункт будет разделен на две части, и по отдельность опишу реализацию каждого. Я рассмотрю, только то, что считаю самым необходимым, иначе статья получиться очень громоздкой, тем более, что большая часть программы не представляет собой ничего трудного.

Клиентская часть


Клиентскую часть я начну описывать по порядку, так что первым делом мы рассмотрим создание меню, если быть точнее — реализацию сворачивание/разворачивание субменю. Данный фрагмент очень прост и практически не требует объяснений, так что я опишу только логику кода.

<Button
    android:id="@+id/alphabet"
    android:layout_width="@dimen/b_size"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="@string/b_alphabet"
    android:visibility="gone"
    android:onClick="Alphabet"/>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <Button
        android:id="@+id/english1"
        android:layout_width="@dimen/b_size2"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/b_english"
        android:visibility="gone"
        android:onClick="TestingWords"/>
    <Button
        android:id="@+id/transcription1"
        android:layout_width="@dimen/b_size2"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/b_transcription"
        android:visibility="gone"
        android:onClick="TestingWords"/>
    <Button
        android:id="@+id/russian1"
        android:layout_width="@dimen/b_size2"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/b_russian"
        android:visibility="gone"
        android:onClick="TestingWords"/>
</LinearLayout>

Как видно из фрагмента кода xml-файла, изначально все пункты субменю скрыты, далее, при нажатии на соответствующую кнопку, они становятся видимыми, а другие пункты, которые были активны, сворачиваются, внизу представлена одна из функций, остальные практически ничем не отличаются.

public void Alphabet(View view) {
    click_alphabet = !click_alphabet;
    if(Btn_Null())
        Btn_Gone();
    btn_english = (Button) findViewById(R.id.english1);
    btn_transcription = (Button) findViewById(R.id.transcription1);
    btn_russian = (Button) findViewById(R.id.russian1);
    if(click_alphabet) {
        btn_english.setVisibility(View.VISIBLE);
        btn_transcription.setVisibility(View.VISIBLE);
        btn_russian.setVisibility(View.VISIBLE);
        btn_alphabet.setText("В АЛФАВИТНОМ ПОРЯДКЕ -");
    } else {
        Btn_Gone();
        btn_alphabet.setText("В АЛФАВИТНОМ ПОРЯДКЕ +");
    }
}

Переменная click_alphabet — проверяет, было ли повторное нажатие на кнопку, если «да», то субменю, за который отвечает данный пункт, сворачивается.

Функция Btn_Null() — проверяет инициализицию всех кнопок.
Функция Btn_Gone() — сворачивает абсолютно все пункты/подпункты меню.

Теперь рассмотрим добавление слова в БД на сервер и на смартфон. Все слова, которые пользователь себе добавил, хранятся в классе TreeMap<String, TransRus>, ключом для данного класса является английское слово, а транскрипция и перевод хранятся в классе TransRus.

public class TransRus {
    private String transcription;
    private String russian;
    boolean error;
    TransRus() {
        transcription = new String("");
        russian = new String("");
        error = false;
    }
    void getTranscription(String tr) {
        transcription = tr;
    }
    void getRussian(String rs) {
        russian = rs;
    }
    String setTranscription() {
        return transcription;
    }
    String setRussian() {
        return russian;
    }
    void getError(boolean t) {
        error = t;
    }
    boolean setError() {
        return error;
    }
}

Класс TreeMap обернут в оболочку класса CollectionWords, также для него был написан компаратор, который игнорирует регистр букв.

class ComparatorNotRegister implements Comparator<String> {  public int compare(String str1, String str2) {
        return str1.compareToIgnoreCase(str2);
    }
}
public class CollectionWords {
    static TreeMap<String, TransRus> coll_words = null;
    static final String file1 = "english";
    static final String file2 = "transcription";
    static final String file3 = "russian";
    static void InitializationCollWords() {
        if(coll_words != null)
            return;
        coll_words = new TreeMap<String, TransRus>(new ComparatorNotRegister());
    }
    static void PutCollWords(String english, String transcription, String russian) {
        TransRus tr = new TransRus();
        tr.getTranscription(transcription);
        tr.getRussian(russian);
        coll_words.put(english, tr);
    }
    static void ChangedWordEng(String old_english, String new_english, String transcription, String russian) {
        TransRus temp = coll_words.get(old_english);
        coll_words.remove(old_english);
        PutCollWords(new_english, transcription, russian);
    }
    static void DeleteWords(String eng) {
        coll_words.remove(eng);
    }
    static void WriteWords(AppCompatActivity t) {
        try(
        BufferedWriter eng   = new BufferedWriter(new OutputStreamWriter(t.openFileOutput(file1, t.MODE_PRIVATE)));
        BufferedWriter trans = new BufferedWriter(new OutputStreamWriter(t.openFileOutput(file2, t.MODE_PRIVATE)));
        BufferedWriter rus   = new BufferedWriter(new OutputStreamWriter(t.openFileOutput(file3, t.MODE_PRIVATE)))) {
            for(Map.Entry<String, TransRus> me : CollectionWords.AllWords()) {
                eng.write(me.getKey() + "\n");
                trans.write(me.getValue().setTranscription() + "\n");
                rus.write(me.getValue().setRussian() + "\n");
            }
        } catch (FileNotFoundException e) {
            Log.d("MyLog", "WF: " + e);
        } catch (IOException e) {
            Log.d("MyLog", "WIOE: " + e);
        } catch(NullPointerException e) {
            Log.d("MyLog", "WN: " + e);
        } catch (Exception e) {
            Log.d("MyLog", "WE: " + e);
        }
    }
    static void ReadWords(AppCompatActivity t) {
        try(
        BufferedReader eng = new BufferedReader(new InputStreamReader(t.openFileInput(file1)));
        BufferedReader trans = new BufferedReader(new InputStreamReader(t.openFileInput(file2)));
        BufferedReader rus = new BufferedReader(new InputStreamReader(t.openFileInput(file3))))  {
            String str_eng;
            String str_trans;
            String str_rus;
            while(((str_eng = eng.readLine())       != null) &&
                    ((str_trans = trans.readLine()) != null) &&
                    ((str_rus = rus.readLine())     != null)) {
                CollectionWords.PutCollWords(str_eng, str_trans, str_rus);
            }
            Log.d("MyLog", "Hyi tam");
        } catch (FileNotFoundException e) {
            Log.d("MyLog", "RF: " + e);
        } catch (IOException e) {
            Log.d("MyLog", "RIO: " + e);
        } catch(NullPointerException e) {
            Log.d("MyLog", "RN: " + e);
        }
    }
    static Set<Map.Entry<String, TransRus>> AllWords() {
        return coll_words.entrySet();
    }
}

Данный класс не представляет ничего сложного, как можно заметить, все слова хранятся в текстовых файлах english, transcription и russian, для этого следовало бы использовать БД, но я не стал сильно заморачиваться, т.к. все и так работает. Функции WriteWords, ReadWords служат для сохранения слов, здесь следует отметить, только то, что, чтобы функция работала, ей нужно будет передать указатель this, из класса, который ее вызвал. Сразу бросается в глаза, тот факт, что все функции статические, это было сделано специально, чтобы не дублировать класс много раз, все остальное понятно и комментарии будут лишними. Теперь, когда дополнительные оболочки рассмотрены, можно описать функцию добавления.

public void AddWord(View view) {
    CollectionWords.InitializationCollWords();
    if(english_language.getText().length() == 0 || russian_language.getText().length() == 0) {
        Toast toast = Toast.makeText(getApplicationContext(),
                "Заполните обязательные поля!", Toast.LENGTH_SHORT);
        toast.show();
        return;
    }
    if(status != 0 && status == MainActivity.INT_CHG) {
        CollectionWords.ChangedWordEng(old_english, english_language.getText().toString(),
                transcription_language.getText().toString().length() == 0 ? "-" : transcription_language.getText().toString(),
                russian_language.getText().toString());
        Back(null);
    }
    CollectionWords.PutCollWords(english_language.getText().toString(),
            (transcription_language.getText().toString().length() == 0 ? "-" : transcription_language.getText().toString()),
            russian_language.getText().toString());
    ClientAddWords caw = new ClientAddWords();
    caw.ServerAddWords("1(!!)" + english_language.getText().toString() + "(!!)" +
                    (transcription_language.getText().toString().length() == 0 ? "-" : transcription_language.getText().toString())
                    + "(!!)" +
                    russian_language.getText().toString());
    english_language.setText("");
    transcription_language.setText("");
    russian_language.setText("");
}

Из фрагмента кода видно, что обязательными полями здесь являются enlish_language и russian_language, если они не заполнены, то добавления слова не произойдет, функция завершится и всплывет сообщение с подсказкой. Как я говорил ранее, для добавления и изменения слов, используется один xml-файл, а соответственно и класс тоже, поэтому здесь присутствует второй оператор if, который проверяет переменную status, хранящее значение о том, был ли осуществлен вход в данную функцию, с целью изменения слова.
Особый интерес представляет собой класс ClientAddWords, который служит для отправки слова на сервер. Т.к. на данный момент Android Studio требует, чтобы всё, что связанно с сетью происходило в отдельном потоке, я решил создать класс, который за это отвечает. В программе присутствует еще один, возвращающий слов с сервера.

Как видно из кода, сначала создается объект ClientAddWords, потом объект вызывает функцию, ей передается строка, разделенная символом «(!!)», этот символ отделяет код операции, предназначенный для сервера, в данном случае это «1», а также английское слово, транскрипцию и перевод, это служит для того, чтобы сервер мог правильно отделить данные друг от друга, как вы уже догадались именно эта строка и передается серверу.

Создание класса, для работы с сетью, очень удобно, ведь подключение клиента к серверу идет именно в тот момент, когда вызывается функция ServerAddWords, а после передачи, клиент отключается от серверного приложения, так что дополнительной нагрузки на сервер нет. Ниже приведен класс ClientAddWords.

public class ClientAddWords extends Thread {
    String str_user;
    void ServerAddWords(String str) {
        str_user = str;
        start();
    }
    public void run() {
        Log.d("MyLog", "Run Client");
        InetAddress addr = null;
        try {
            addr = InetAddress.getByName("192.168.1.208");
        } catch (UnknownHostException e) {
            Log.d("MyLog", "ServerAddWords ClientAddWords: " + e);
        }
        Client c;
        try {
            c = new Client(addr);
        } catch (IOException e) {
            Log.d("MyLog", "Socket failed: " + e);
            return;
        }
        c.Out(str_user);
        c.Close();
    }
}

Как видно, в данной реализации кода, нет ничего сложного и он не требует дополнительных разъяснений, код класса Client, представлен ниже.

public class Client extends Thread {
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;
    InetAddress addr;
    public String In() throws IOException {
        return in.readLine();
    }
    public void Out(String str) {
        out.println(str);
    }
    public Client(InetAddress addr) throws IOException {
        this.addr = addr;
        Log.d("MyLog", "Making client");
        try {
            socket = new Socket(addr, 8080);
        } catch (IOException e) {
            Log.d("MyLog", "Socket failed: " + e);
            throw e;
        }
        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
        } catch (Exception e) {
            Log.d("MyLog", "In/Out: " + e);
            try {
                socket.close();
            }
            catch (IOException e2) {
                Log.d("MyLog", "Client: Socket not closed");
            }
        }
    }
    public void Close() {
        try {
            socket.close();
            in.close();
            out.close();
        } catch (IOException e) {
            Log.d("MyLog", "Close Client: " + e);
        }
    }
}

Здесь также нет интересных моментов, данный пример кода можно найти на любом сайте посвященный сетевому программированию.

Сейчас мы рассмотрим класс, отвечающий за изменение и удаление слов, как я уже не раз говорил они используют один xml-файл, и самое интересное в его реализации — это заполнение layout'a button'ами.

<ScrollView
  android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background_find" >
    <LinearLayout
        android:id="@+id/ll_find"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    </LinearLayout>
</ScrollView>

В самом xml-файле интерес представляет только этот кусок кода, здесь видно что layout'у задается id — ll_find, это нужно для того, чтобы создавать кнопки программно.

class MyButton extends Button {
    private String str_eng;
    private String str_trans;
    private String str_rus;
    private int index;
    public MyButton(Context context) {
        super(context);
    }
    void setEnglish(String eng) {
        str_eng = eng;
    }
    void setTranscription(String trans) {
        str_trans = trans;
    }
    void setRussian(String rus) {
        str_rus = rus;
    }
    void setIndex(int id) { index = id; }
    String getEnglish() { return str_eng; }
    String getTranscription() {
        return str_trans;
    }
    String getRussian() {
        return str_rus;
    }
    int    getIndex() { return index; }
}

Отмечу сразу данный класс, как не трудно догадаться он предназначен лишь для хранения значений карточки, а именно английское слов, его транскрипцию и перевод.

Первая функция, которую следует описать — это CreateButton, код представлен ниже.

public MyButton CreateButton(int i, String eng, String trans, String rus) {
    final MyButton btnNew = new MyButton(this);
    btnNew.setBackgroundResource(R.drawable.background_button);
    btnNew.setText(eng + " - " + rus);
    btnNew.setEnglish(eng);
    btnNew.setTranscription(trans);
    btnNew.setRussian(rus);
    btnNew.setIndex(i);
    if(status == MainActivity.INT_DEL) {
        btnNew.setCompoundDrawablesWithIntrinsicBounds(R.drawable.icon_delete_button, 0, 0, 0);
        btnNew.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder ad;
                Find context;
                context = Find.this;
                String title = "Удаление слова";
                String message = "Вы уверены?";
                String button1String = "Да";
                String button2String = "Нет";
                ad = new AlertDialog.Builder((Context) context);
                ad.setTitle(title);
                ad.setMessage(message);
                ad.setPositiveButton(button1String, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int arg1) {
                        CollectionWords.DeleteWords(btnNew.getEnglish());
                        ll_layout.removeView(btnNew);
                    }
                });
                ad.setNegativeButton(button2String, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int arg1) {
                    }
                });
                ad.show();
            }
        });
    }
    else {
        btnNew.setCompoundDrawablesWithIntrinsicBounds(R.drawable.icon_changed_button, 0, 0, 0);
        btnNew.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("MyLog", "Changed Words");
                ChangedWord((MyButton) v);
                ll_layout.removeView((MyButton) v);
            }
        });
    }
    return btnNew;
}

Здесь есть пару интересных моментов, а именно изменение заднего фона кнопки, далее, в зависимости от того, с какой целью посещается данный класс, это создание иконки у кнопки, за это отвечает функция setCompoundDrawablesWithIntrinsicBounds, а также реализация события setOnClickListener, тут либо создается диалоговое окно и происходит удаление слова и btnNew, либо вызывается функция ChangedWord, которая открывает активити, предназначенное для изменения слова.

Следующая функция, про которую следует рассказать, это ShowViewWords, ее предназначение занесение button'ов в layout.

public void ShowViewWords(LinearLayout.LayoutParams lParams, String sub_str) {
    int i = 0;
    String first_chr = new String("");
    for(Map.Entry<String, TransRus> me : CollectionWords.AllWords()) {
        if(sub_str.length() != 0 && (me.getKey().toLowerCase().indexOf(sub_str.toLowerCase())                == -1 &&
                                     me.getValue().setRussian().toLowerCase().indexOf(sub_str.toLowerCase()) == -1))
            continue;
        if(!first_chr.equals(String.valueOf(me.getKey().toUpperCase().charAt(0)))) {
            first_chr = String.valueOf(me.getKey().toUpperCase().charAt(0));
            TextView temp = new TextView(this);
            temp.setText(first_chr + ":");
            temp.setTextSize(25f);
            ll_layout.addView(temp, i, lParams);
            i++;
        }
        ll_layout.addView(CreateButton(i, me.getKey(), me.getValue().setTranscription(), me.getValue().setRussian()),
                i, lParams);
        i++;
    }
    if(i == 0) {
        LinearLayout.LayoutParams l = CreateParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER_VERTICAL);
        TextView not_found = new TextView(this);
        not_found.setText("Ничего не найдено");
        ll_layout.addView(not_found, l);
    }
}

Здесь также происходит создание и добавление TextView'а, который хранит значение первой буквы слова, отличающегося от первой буквы следующего слова. Постарался объяснить как можно лучше, но на всякий случай в пример приведу изображение.

image

За это отвечает вот этот кусок кода.

if(!first_chr.equals(String.valueOf(me.getKey().toUpperCase().charAt(0)))) {
            first_chr = String.valueOf(me.getKey().toUpperCase().charAt(0));
            TextView temp = new TextView(this);
            temp.setText(first_chr + ":");
            temp.setTextSize(25f);
            ll_layout.addView(temp, i, lParams);
            i++;
        }

Изначально переменной first_chr, присваивается пустая строка, так что если цикл проходит первый раз, то в нее обязательно занесется значение.

За условие цикла отвечает данная строка.

for(Map.Entry<String, TransRus> me : CollectionWords.AllWords()) 

О предназначении первого оператора if, я расскажу позже, т. к. это относится к поиску, а о поиске расскажу в последнюю очередь.

 ll_layout.addView(CreateButton(i, me.getKey(), me.getValue().setTranscription(), me.getValue().setRussian()),
                i, lParams);

А за добавление в ll_layout, отвечает это конструкция, здесь вызывается функция CreateButton, рассмотренная выше и возвращающая значение MyButton, далее добавляется индекс и параметры этой кнопки.

Функция ShowViewWords, вызывается из функции CreatesButton, а функция CreatesButton вызывается уже из onCreate. Код CreatesButton приведен ниже.

public void CreatesButton(String str_find) {
    ll_layout.removeAllViews();
    LinearLayout.LayoutParams lParams = CreateParams(LinearLayout.LayoutParams.MATCH_PARENT,
                                                     LinearLayout.LayoutParams.WRAP_CONTENT,
                                                     Gravity.LEFT);
    lParams.topMargin = 1;
    ShowViewWords(lParams, str_find);
}

Код функции CreateParams:

public LinearLayout.LayoutParams CreateParams(int width, int height, int gravity) {
    LinearLayout.LayoutParams lParams = new LinearLayout.LayoutParams(width, height);
    lParams.gravity = gravity;
    return lParams;
}

На данном этапе описании программы, я подошел к реализации класса TestWords, который отвечает за тестирование слов. Как и обычно в нем нет ничего сложного, но есть пару моментов, которые следует рассмотреть. Самый первый, это выбор пользователем теста, как ни трудно догадаться всего может быть девять вариантов тестирования, а именно заданный порядок слов(в алфавитном, в обратном и рандом) и объект тестирования(английски, транскрипция, русский). Так что самой первой вызывается функция StatusTest. Она определяет по коду, какие из Edit'ов должны быть заблокированы, и инициализирует переменную индексом, под которым стоит слово.

public void StatusTest() {
    switch (status) {
        case MainActivity.INT_ALPH_ENG:
            edit_transcription.setRawInputType(0x00000000);
            edit_russian.setRawInputType(0x00000000);
            next_words = 0;
            break;
        case MainActivity.INT_ALPH_TRANS:
            edit_english.setRawInputType(0x00000000);
            edit_russian.setRawInputType(0x00000000);
            next_words = 0;
            break;
        case MainActivity.INT_ALPH_RUS:
            edit_english.setRawInputType(0x00000000);
            edit_transcription.setRawInputType(0x00000000);
            next_words = 0;
            break;
        case MainActivity.INT_REVS_ENG:
            edit_transcription.setRawInputType(0x00000000);
            edit_russian.setRawInputType(0x00000000);
            next_words = CollectionWords.coll_words.size() - 1;
            break;
        case MainActivity.INT_REVS_TRANS:
            edit_english.setRawInputType(0x00000000);
            edit_russian.setRawInputType(0x00000000);
            next_words = CollectionWords.coll_words.size() - 1;
            break;
        case MainActivity.INT_REVS_RUS:
            edit_english.setRawInputType(0x00000000);
            edit_transcription.setRawInputType(0x00000000);
            next_words = CollectionWords.coll_words.size() - 1;
            break;
        case MainActivity.INT_RAND_ENG:
            rand_next_words = new Random();
            next_words = MethodRandomWords();
            edit_transcription.setRawInputType(0x00000000);
            edit_russian.setRawInputType(0x00000000);
            break;
        case MainActivity.INT_RAND_TRANS:
            rand_next_words = new Random();
            next_words = MethodRandomWords();
            edit_english.setRawInputType(0x00000000);
            edit_russian.setRawInputType(0x00000000);
            break;
        case MainActivity.INT_RAND_RUS:
            rand_next_words = new Random();
            next_words = MethodRandomWords();
            edit_english.setRawInputType(0x00000000);
            edit_transcription.setRawInputType(0x00000000);
            break;
    }
}

В случае алфавитного порядка переменной next_words, присваивается 0, обратный порядок — это размер класса TreeMap минус 1 и рандомный порядок, специальная функция, которая возвращает случайное значение в next_words.

Функция MethodRandomWords служит для того, чтобы не вернуть одно и тоже случайное значение несколько раз.

После инициализации всех Edit'ов и вызова функции StatusTest, выполняется метод ReadWord.

public void ReadWord() {
    last_words++;
    amount_words.setText(last_words + "/" + CollectionWords.coll_words.size());
    switch (status) {
        case MainActivity.INT_ALPH_ENG:
            nw = VecNextWord(next_words);
            edit_english.setText("");
            edit_transcription.setText(nw.getValue().setTranscription());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_ALPH_TRANS:
            nw = VecNextWord(next_words);
            edit_transcription.setText("");
            edit_english.setText(nw.getKey());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_ALPH_RUS:
            nw = VecNextWord(next_words);
            edit_russian.setText("");
            edit_english.setText(nw.getKey());
            edit_transcription.setText(nw.getValue().setTranscription());
            break;
        case MainActivity.INT_REVS_ENG:
            nw = VecNextWord(next_words);
            edit_english.setText("");
            edit_transcription.setText(nw.getValue().setTranscription());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_REVS_TRANS:
            nw = VecNextWord(next_words);
            edit_transcription.setText("");
            edit_english.setText(nw.getKey());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_REVS_RUS:
            nw = VecNextWord(next_words);
            edit_russian.setText("");
            edit_english.setText(nw.getKey());
            edit_transcription.setText(nw.getValue().setTranscription());
            break;
        case MainActivity.INT_RAND_ENG:
            nw = VecNextWord(next_words);
            edit_english.setText("");
            edit_transcription.setText(nw.getValue().setTranscription());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_RAND_TRANS:
            nw = VecNextWord(next_words);
            edit_transcription.setText("");
            edit_english.setText(nw.getKey());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_RAND_RUS:
            nw = VecNextWord(next_words);
            edit_russian.setText("");
            edit_english.setText(nw.getKey());
            edit_transcription.setText(nw.getValue().setTranscription());
            break;
    }
    status_true_word.setText("Статус:-");
}

Цель этого метода, загрузить первое слов, за это отвечает функция VecNextWord, параметр которой индекс и возвращаемым значением является Map.Entry<String, TransRus>, а также указать количество слов и обнулить статус.

Следующую роль играют всего две функции, Check — проверяет правильность введенного слова и при случае меняет Статус, NextWord — возвращает следующее по списку слово, в зависимости от выбранного порядка. Код функций приведен ниже.

public void Check(View view) {
    check_bool = true;
    Log.d("MyLog", "Status: " + status);
    switch (status) {
        case MainActivity.INT_ALPH_ENG:
            Log.d("MyLog", "Check()");
            if(edit_english.getText().toString().equals(nw.getKey())) {
                Log.d("MyLog", "True");
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                Log.d("MyLog", "False");
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_ALPH_TRANS:
            Log.d("MyLog", "Check()");
            if(edit_transcription.getText().toString().equals(nw.getValue().setTranscription())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_ALPH_RUS:
            Log.d("MyLog", "Check()");
            if(edit_russian.getText().toString().equals(nw.getValue().setRussian())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_REVS_ENG:
            Log.d("MyLog", "Check()");
            if(edit_english.getText().toString().equals(nw.getKey())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_REVS_TRANS:
            Log.d("MyLog", "Check()");
            if(edit_transcription.getText().toString().equals(nw.getValue().setTranscription())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_REVS_RUS:
            Log.d("MyLog", "Check()");
            if(edit_russian.getText().toString().equals(nw.getValue().setRussian())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_RAND_ENG:
            Log.d("MyLog", "Check()");
            if(edit_english.getText().toString().equals(nw.getKey())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_RAND_TRANS:
            Log.d("MyLog", "Check()");
            if(edit_transcription.getText().toString().equals(nw.getValue().setTranscription())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
        case MainActivity.INT_RAND_RUS:
            Log.d("MyLog", "Check()");
            if(edit_russian.getText().toString().equals(nw.getValue().setRussian())) {
                status_true_word.setText("Статус: Верно");
                Log.d("MyLog", "next_words: " + next_words);
                nw.getValue().getError(true);
            } else {
                status_true_word.setText("Статус: Не верно");
            }
            break;
    }
}

public void NextWord(View view) throws InterruptedException {
    if(last_words >= CollectionWords.coll_words.size()) {
        ResultTestGo();
        return;
    }
    if(!check_bool && last_words != 0)
        Check(view);
    AddWordInTable(nw.getValue().setError());
    last_words++;
    amount_words.setText(last_words + "/" + CollectionWords.coll_words.size());
    switch (status) {
        case MainActivity.INT_ALPH_ENG:
            next_words++;
            nw = VecNextWord(next_words);
            edit_english.setText("");
            edit_transcription.setText(nw.getValue().setTranscription());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_ALPH_TRANS:
            next_words++;
            nw = VecNextWord(next_words);
            edit_transcription.setText("");
            edit_english.setText(nw.getKey());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_ALPH_RUS:
            next_words++;
            nw = VecNextWord(next_words);
            edit_russian.setText("");
            edit_english.setText(nw.getKey());
            edit_transcription.setText(nw.getValue().setTranscription());
            break;
        case MainActivity.INT_REVS_ENG:
            next_words--;
            nw = VecNextWord(next_words);
            edit_english.setText("");
            edit_transcription.setText(nw.getValue().setTranscription());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_REVS_TRANS:
            next_words--;
            nw = VecNextWord(next_words);
            edit_transcription.setText("");
            edit_english.setText(nw.getKey());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_REVS_RUS:
            next_words--;
            nw = VecNextWord(next_words);
            edit_russian.setText("");
            edit_english.setText(nw.getKey());
            edit_transcription.setText(nw.getValue().setTranscription());
            break;
        case MainActivity.INT_RAND_ENG:
            next_words = MethodRandomWords();
            nw = VecNextWord(next_words);
            edit_english.setText("");
            edit_transcription.setText(nw.getValue().setTranscription());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_RAND_TRANS:
            next_words = MethodRandomWords();
            nw = VecNextWord(next_words);
            edit_transcription.setText("");
            edit_english.setText(nw.getKey());
            edit_russian.setText(nw.getValue().setRussian());
            break;
        case MainActivity.INT_RAND_RUS:
            next_words = MethodRandomWords();
            nw = VecNextWord(next_words);
            edit_russian.setText("");
            edit_english.setText(nw.getKey());
            edit_transcription.setText(nw.getValue().setTranscription());
            break;
    }
    status_true_word.setText("Статус:-");
    check_bool = false;
}

Я не думаю, что у кого-то могут возникнуть трудности с кодом этих функция, но необходимо отметить пару моментов. Как было показано на изображении выше, после прохождения или завершения теста, формируется Html-таблица, для удобного отображения результата. Исход код таблицы, для быстродействия программы, формируется во время прохождения теста, при помощи функции AddWordInTable(nw.getValue().setError()), ей передается булево значение.

public void AddWordInTable(boolean temp) {
    table_result += "<tr><td  " + (temp ? "bgcolor=\"#008000\"" : "bgcolor=\"#FF0000\"")  + ">" + nw.getKey()  +
                    "</td><td " + (temp ? "bgcolor=\"#008000\"" : "bgcolor=\"#FF0000\"")  + ">" + nw.getValue().setTranscription() +
                    "</td><td " + (temp ? "bgcolor=\"#008000\"" : "bgcolor=\"#FF0000\"")  + ">" + nw.getValue().setRussian() +
                    "</td><td " + (temp ? "bgcolor=\"#008000\"" : "bgcolor=\"#FF0000\"")  + ">" + (temp ? "Верно" : "Не верно") + "</td></tr>\n";
}

Начало таблицы формируется в функции onCreatе, а конец уже в классе ResultTest, который выводит результат прохождения теста.
После того как пользователь дошел до последнего слова или завершил тест, открывается активтити, где выведен результат, как отмечалось выше это класс ResultTest, я не думаю, что он требует дополнительных комментариев, но следует заметить, что для построения Html-таблицы была использована функция loadDataWithBaseURL(null, TestWords.table_result, «text/html», «UTF-8», null), а также применен WebView.

Вот и подобрался я к самому интересному, а именно к загрузке слов с сервера. Класс отвечающий за это называется Load, на его реализацию ушло примерно 18 часов, т.к. там используется класс RecyclerView, а это для меня было что-то новое и в начале он вел меня в ступор, но в итоге, я все же с ним разобрался. Так что будет правильным, если свое повествование, о создании класса Load, я начну именно с него.

Первым делом хочется сказать, что этот класс получился достаточно большим, как ни старался его уменьшить, но все же рабочим. Для его использования был создан xml-файл с CheckBox'ом и TextView'ом, изначально скрытым. Здесь TextView служит для создания алфавита указателя, как можно увидеть на картинке выше. О том как он создается, я уже говорил, но здесь есть один момент, без которого, создание алфавита не получится и его опишу чуть позже.

Так же хочу отметить, что если вы до этого не имели дело с классом RecyclerView, то не стоит начинать его изучение с этой статьи, т.к. подробного о его применении я описать не смогу, так что лучше погуглите.

Сначала я перегрузил три функции, onCreateViewHolder, onBindViewHolder, getItemCount, а также класс ViewHolder. GetItemCount не представляет особого интереса, т.к. она возвращает только размерность. Начну свое описание с класса ViewHolder.

public static class ViewHolder extends RecyclerView.ViewHolder {
    public CheckBox chkbox;
    public TextView tv_alph;
    public ViewHolder(View v) {
        super(v);
        chkbox = (CheckBox) v.findViewById(R.id.rv_chkbox);
        tv_alph = (TextView) v.findViewById(R.id.tv_alph);
    }
}

Этот класс довольно прост и не требует никаких комментариев. Далее идет функция onCreateViewHolder, она служит для создания объектов ViewHolder.

public RecyclerLoad.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.content_checkbox, parent, false);
    ViewHolder vh = new ViewHolder(v);
    return vh;
}

Теперь рассмотрим функцию onBindViewHolder.

public void onBindViewHolder(final ViewHolder holder, final int position) {
    if(index_alph[position]) {
        holder.tv_alph.setText(Alph[position]);
        holder.tv_alph.setVisibility(View.VISIBLE);
    } else holder.tv_alph.setVisibility(View.GONE);
    holder.chkbox.setText(Eng_Array[position] + " - [" +
            Trans_Array[position] + "] - " +
            Rus_Array[position]);
    if(this_load.menu_load_1 != null)
        this_load.menu_load_1.setTitle("Загрузить все слова (" + Integer.toString(amount) + ")");
    holder.chkbox.setOnCheckedChangeListener(null);
    holder.chkbox.setChecked(checked_box[position]);
    holder.chkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if(isChecked)
                checked_words_load++;
            else
                checked_words_load--;
            checked_box[position] = isChecked;
            if(this_load.menu_load_2 != null)
                this_load.menu_load_2.setTitle("Заг. выделенные слова (" + Integer.toString(checked_words_load) + ")");
        }
    });
    if (position >= amount - 1) {
        Log.d("MyLog", "Подкачка!!!");
        LoadWords(sub_str);
        onBind = true;
    }
}

Что интересного здесь можно отметить, первое — объект holder заполняется данными, второе — чтобы не было дублирования нажатия CheckBox'ов вызываются функции setOnCheckedChangeListener и setChecked, далее во всех CheckBox'ах перегружается функция onCheckedChanged, для того чтобы в пункте меню менялось значение «количетсво» и последнее — это оператора if проверяющий условие предпоследнего слова, как только пользователь до него доходит вызывается функция подгрузки слов LoadWords.

Внимательный читатель наверное неоднократно заметил переменную sub_str, настало время рассказать о ней подробнее. Это переменная String, он хранит строку введенную пользователем в Edit'е «Найти: ». Т.к. в программе реализован живой поиск, то она передаются в функцию после каждого изменения текста в данном Edit'e, код представлен ниже.

find_words.addTextChangedListener(new TextWatcher() {
    public void afterTextChanged(Editable s) {
    }
    public void beforeTextChanged(CharSequence s, int start,
                                  int count, int after) {
    }
    public void onTextChanged(CharSequence s, int start,
                              int before, int count) {
        FindWord(null);
    }
});
public void FindWord(View view) {
    mAdapter.Find(find_words.getText().toString());
    mAdapter.notifyDataSetChanged();
}

Из кода видно, что после изменения текста вызывается функция FindWord, в которой вызывается метод Find, класса RecyclerLoad, где загружаются слова с сервера, которые содержат подстроку введенную пользователем, а затем вызывается функция обновления.

public void Find(String sub_str) {
    this.sub_str = sub_str;
    amount = 0;
    Eng_Array = null;
    Trans_Array = null;
    Rus_Array = null;
    checked_box = null;
    index_alph = null;
    Alph = null;
    LoadWords(sub_str);
}

Перед вызовом LoadWords, все переменные класса RecyclerLoad обновляются, т.к. загружаются совершенно новые данные.

public void LoadWords(String sub_str) {
    ClientLoadWords clw = new ClientLoadWords();
    clw.LoadWords("2(!!)" + (sub_str.length() != 0 ? sub_str : "(--)"), amount);
    try {
        clw.join();
    } catch (InterruptedException e) {
        Log.d("MyLog", "ShowViewWords: " + e);
    }
    if(clw.int_error == -1) {
        Toast toast = Toast.makeText(this_load.getApplicationContext(),
                "Нет соединения с сервером!", Toast.LENGTH_SHORT);
        toast.show();
    }
    amount += clw.amount;
    Log.d("MyLog", "amount = " + amount);
    String [] temp_Eng_Array    = new String[amount];
    String [] temp_Trans_Array  = new String[amount];
    String [] temp_Rus_Array    = new String[amount];
    String [] temp_Alph         = new String[amount];
    boolean [] temp_checked_box = new boolean[amount];
    boolean [] temp_index_alph  = new boolean[amount];
    int temp_amount = Eng_Array != null ? Eng_Array.length : 0;
    if(Eng_Array != null)
        for(int i = 0; i < Eng_Array.length; i++) {
            temp_Eng_Array[i]      = Eng_Array[i];
            temp_Trans_Array[i]    = Trans_Array[i];
            temp_Rus_Array[i]      = Rus_Array[i];
            temp_Alph[i]           = Alph[i];
            temp_checked_box[i]    = checked_box[i];
            temp_index_alph[i]     = index_alph[i];
        }
    for(int i = 0; i < clw.amount; i++) {
        temp_Eng_Array[i + temp_amount]      = clw.Eng_Array[i];
        temp_Trans_Array[i + temp_amount]    = clw.Trans_Array[i];
        temp_Rus_Array[i + temp_amount]      = clw.Rus_Array[i];
        temp_checked_box[i + temp_amount]    = false;
        temp_index_alph[i + temp_amount]     = false;
        temp_Alph[i + temp_amount]           = "";
        if(!first_chr.equals(String.valueOf(clw.Eng_Array[i].toUpperCase().charAt(0)))) {
            Log.d("MyLog", "First Chr: " + first_chr + ", me.getKey(): " + clw.Eng_Array[i].toUpperCase().charAt(0) +
                    " boolean: " + first_chr.equals(String.valueOf(clw.Eng_Array[i].toUpperCase().charAt(0))));
            first_chr = String.valueOf(clw.Eng_Array[i].toUpperCase().charAt(0));
            temp_Alph[i + temp_amount] = first_chr + ":";
            temp_index_alph[i + temp_amount] = true;
        }
    }
    for(int i = 0; i < temp_Eng_Array.length; i++) {
        Log.d("MyLog", "temp_Alph: " + temp_Alph[i] + ", temp_index_alph = " + temp_index_alph[i] + ", i = " + i);
    }
    Eng_Array = temp_Eng_Array;
    Trans_Array = temp_Trans_Array;
    Rus_Array = temp_Rus_Array;
    checked_box = temp_checked_box;
    index_alph = temp_index_alph;
    Alph = temp_Alph;
}

Функция LoadWords получилась большой, так что я отмечу самые важные моменты. Во-первых класс ClientLoadWords — это подобие класса ClientAddWords, только он принимает данные с сервера, а не отправляет. Ему так же передается строка с кодом операции и строкой поиска. Второй параметр функции LoadWords, количество слов уже полученных слов, эта переменная нужна, чтобы сервер знал сколько он слов отправил и сколько осталось отправить еще. Код функции LoadWords приведен ниже.

public void run() {
    InetAddress addr = null;
    try {
        addr = InetAddress.getByName("192.168.1.137");
    } catch (UnknownHostException e) {
        Log.d("MyLog", "ClientLoadWords LoadWords 1: " + e);
    }
    Client c;
    try {
        c = new Client(addr);
    } catch (IOException e) {
        Log.d("MyLog", "Socket failed: " + e);
        int_error = -1;
        return;
    }
    c.Out(str_user);
    c.Out(Integer.toString(begin));
    amount = 0;
    try {
        amount = Integer.parseInt(c.In());
    } catch (IOException e) {
        Log.d("MyLog", "LoadWords ClientLoadWords 3: " + e);
    }
    Log.d("MyLog", "Amount: " + amount);
    Id_Array    = new int[amount];
    Eng_Array   = new String[amount];
    Trans_Array = new String[amount];
    Rus_Array   = new String[amount];
    try {
        for (int i = 0; i < amount; i++) {
            Id_Array[i]    = Integer.parseInt(c.In());
            Eng_Array[i]   = c.In();
            Trans_Array[i] = c.In();
            Rus_Array[i]   = c.In();
        }
    } catch (IOException e) {
        Log.d("MyLog", "LoadWords ClientLoadWords 4: " + e);
    }
    Sort();
}

Самое интересное в этой функции, это получение данных. С начала, клиентское приложение получает количество отправляемых слов, затем создаются массивы и потом уже в цикле for, загружаются все слова, а далее, как ни трудно догадаться они сортируются.

Теперь вернемся в функцию LoadWords. Конструкция clw.join() ожидает завершения потока в классе ClientLoadWords, и только затем продолжает выполнение остальных операторов. Так же если объект clw возвращает код ошибка равный -1, то выводится подсказка и функция завершает свое выполнение. Далее, либо инициализируются массивы и туда заносятся полученные данные, либо уже к этим массивам данные дополняются, а также алфавитный указатель сразу жестко закрепляется и булевы значени отображения TextView'а заносятся в массив index_alph, иначе бы TextView дублировался.

Я думаю, на этом рассмотрение клиентского приложения можно закончить, т.к. самые интересные моменты, такие как отправка данных, живой поиск, создание теста, подгрузка слов и т. д., были рассмотрены, а пункт «Просмотреть слова», можно и пропустить, потому что его создания практически ничем не отличается от вывода результат после окончания теста, по сути это та же Html-таблица, только не цветная, так что приступим к серверному приложению.

Серверное приложение


Серверное приложение, является многопоточным, использует базу данных MySQL и передает данные по средством протокола TCP, это все что нужно знать, чтобы приступить к рассмотрению программа. За принятие/отправку данных отвечает класс ServerOneJabber. Он наследует класс Thread. Полностью приводить код данного класса я не буду, т.к. это не имеет смысла.

public ServerOneJabber(Socket s) throws IOException, SQLException {
    socket = s;
    
    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
    
    sqldb = new SqlDB();
    
    start();
}

Конструктор не представляет ничего сложного, стоит только обратить внимание на объект sqldb, все что о нем следует знать, это то, что он отвечает за связь с БД. Объекты in и out далее оборачиваются в функции In и Out, это сделано для удобства.

public void run() {
    String message_user = new String("");
  
    try {
      message_user = Out();
    } catch(IOException e) {
      System.out.println(e);
    }
    
    System.out.println("message_user: " + message_user);
    
    try {
      switch(message_user.charAt(0)) {
        case '1':
          AddWord(message_user);
          break;
        case '2':
          TransferWord(message_user);
          break;
      }
    } catch(SQLException e) {
        System.out.println(e);
    } catch(IOException e) {
        System.out.println(e);
    }
  }

Предназначение функции run заключается только в определении кода операции и вызова соответствующей функции. Рассмотрим каждую по очереди.

public void AddWord(String str) throws SQLException  {
    String[] str_user = Split(str, "(!!)");
    
    for(int i = 0; i < str_user.length; i++)
      System.out.println("str_user: " + str_user[i]);
    
    sqldb.AddWord(str_user[1], str_user[2], str_user[3]);
  }

В функции AddWord все понятно. В sqldb передаются данные карточки и заносятся в БД с помощью метода AddWord. Метод Split получает на вход строку отправленную клиентом и строку разделитель, она была написана, потому что стандартная функция split класса String работала не правильно, я так и не разобрался почему.

public void TransferWord(String str) throws SQLException, IOException {
    String [] user_str = Split(str, "(!!)");
    
    for(int i = 0; i < user_str.length; i++) {
      System.out.println("user_str: " + user_str[i]);
    }
    
    if(sqldb.CountSQL() == 0) {
      In("0");
      return;
    }
    
    int begin = new Integer(Out());
    
    System.out.println("Loading Words...");
    
    String [] data;
    
    if(user_str[1].equals("(--)"))
      data = sqldb.AllWords();
    else
      data = sqldb.Find(user_str[1]);
    
    read_amount_words = (data.length - begin > 100 ? 100 : data.length - begin);
      
    In(Integer.toString(read_amount_words));
      
    for(int i = begin; i < begin + ((data.length - begin) > 100 ? 100 : data.length); i++) {
      System.out.println("i = " + i + ", data.length = " + data.length);
      
      if(i == data.length)
        break;
        
      String [] str_data = Split(data[i], "(!!)");
        
      for(int j = 0; j < str_data.length; j++)
          System.out.println("str_data: " + str_data[j]);
        
      In(str_data[0]);
      In(str_data[1]);
      In(str_data[2]);
      In(str_data[3]);
    }
  }

Функция TransferWord передает данные клиенту. Первое, что делает функция, это проверяет строку поиска, если пользователь не пользовался поиском, то функция возвращает все слова, иначе только те, которые содержат данную подстроку. После этого проверяется сколько слов было передано и сколько осталось, если в БД осталось больше 100 слов, то передаются следующее 100 слов с отметки, на которой закончилась предыдущая передача, иначе передается остаток.

public class Server {
  static final int PORT = 8080;
  
  static public void main(String[] args) throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Server Started");
    
    try {
      while(true) {
        Socket socket = s.accept();
        try {
          System.out.println("Client Connection");
          new ServerOneJabber(socket);
        } catch(IOException e) {
          socket.close();
        }
      }
    } catch(SQLException e) {
        System.out.println(e);
    } finally {
      s.close();
    }
  }
}

В классе Server содержится метод main, и если к серверному приложению подключается клиент, то создается класс ServerOneJabber в новом потоке.

Заключение


Вот и подошла к завершению данная статья. Теперь, после написания данной программы, я могу учить английские слова, когда сижу на парах, где-нибудь в очереди или же дома. Надеюсь это статья окажется для кого-нибудь полезной, старался описать все как можно подробнее, но это у меня всегда плохо получалось. Спасибо за внимание и потраченное время.
Поделиться с друзьями
-->

Комментарии (12)


  1. bodhisattva
    10.06.2016 12:54
    +2

    AnkiDroid. А лучше Memrise.


    1. danl
      10.06.2016 13:28

      поддерживаю. сделать своё приложение это здорово, но для словарного запаса всё-таки лучше AnkiDroid с какой-нибудь толковой колодой карточек (не с переводом а с толкованием слова на том же языке), типа «топ 5000 по частоте»


  1. lizarge
    10.06.2016 13:25
    +1

    Ваша программа делает процесс очень сложным, с бумажками проще и веселее. 99% функционала нужно убрать и сделать все как в бумажной версии… Паки слов должны быть уже готовы и разбиты на уровни. Про UI говорить не буду.

    Ну это если рассматривать программу как продукт, для обучения программированию и just 4 fun то все отлично!


    1. spmbt
      10.06.2016 14:11

      Ага, "… просто скажите себе: «перчатки»." Т.е. написать полезно, но лучше сделать это предельно лаконично: таблица переводов, функции экспозиции, таблица результатов. в одну JS-страницу поместятся с localStorage, даже сервер и интернет не обязателен.


    1. Distribution
      13.06.2016 12:20

      Думаю, что с процентами функционала вы загнули, а на счет бумажек и карточек, я так не думаю, пробывал и то и это, но как говорится на вкус и цвет.


  1. vait
    11.06.2016 08:39

    Самое интересное для меня была реализации транскрипции, но не увидел. Может есть какие-то задумки?


    1. Distribution
      13.06.2016 12:24

      А что именно в создании транскрипции вас заинтересовало?


      1. vait
        13.06.2016 22:26

        Хранить ее в юникоде — это понятно. А вот откуда ее брать и как вводить, ведь там набор специальных символов, и клавиатура нужна особенная.


  1. elkyzmich
    11.06.2016 22:18

    На самом деле не ясна цель статьи. Не раскрывается ни изучение языка, ни разработка приложения.


    1. Distribution
      13.06.2016 12:31

      Мне кажется, для изучения языка следует прочитать книгу, а что касается разработки приложения, здесь описан процесс создания того, что мне показалось необходимым и интересным, и то, как я с этим справлялся, я просто не могу представить, что вы хотели увидеть еще?


  1. evilbug
    12.06.2016 12:06

    проект можно как то получить для ознакомления?


    1. Distribution
      13.06.2016 12:31

      Что именно вам нужно?