Цель статьи


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

В данной статье будут рассмотрены следующие вопросы:

  • Кастомная клавиатура под Android
  • Многопоточность
  • Интеграция рекламы в программу

Кастомная клавиатура под Android


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

Начнём:
  1. Создадим новый проект в android studio.
  2. Создадим пару классов
  3. Протестируем приложение

Сделаем простенькую клавиатуру из 9 символов с возможностью удалять эти символы.
Назовём наш проект KeyBoardTest



Выберем пустую активити и начнём



Готово мы создали наш новый проект. Теперь займёмся самым основным, а именно создадим layout файл в папке — res/layout и назовём его keyboard, тут у нас будет размещён внешний вид клавиатуры.

Нарисуем эту клавиатуру:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_gravity="bottom"
        android:background="@color/colorBlue"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:orientation="vertical">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/one"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/one"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/two"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/two"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/three"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/three"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/four"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/four"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/five"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/five"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/six"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/six"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/seven"                
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/seven"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/eight"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/eight"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/nine"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/nine"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
            <TextView
                android:id="@+id/delete"
                android:textColor="@color/colorWhite"
                android:gravity="center"
                android:textSize="30sp"
                android:text="@string/delete"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent"/>
        </LinearLayout>

    </LinearLayout>

</FrameLayout>

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

Назовём этот класс KeyBoardListener.

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

package keyboard.develop.keyboardtest;

import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;

public class KeyBoardListener {

    EditText editText;
    private StringBuilder finalText = new StringBuilder();

    KeyBoardListener(View view, final EditText editText) {
        this.editText = editText;
        TextView one = view.findViewById(R.id.one);
        TextView two = view.findViewById(R.id.two);
        TextView three = view.findViewById(R.id.three);
        TextView four = view.findViewById(R.id.four);
        final TextView five = view.findViewById(R.id.five);
        TextView six = view.findViewById(R.id.six);
        TextView seven = view.findViewById(R.id.seven);
        TextView eight = view.findViewById(R.id.eight);
        TextView nine = view.findViewById(R.id.nine);
        TextView delete = view.findViewById(R.id.delete);
        final LinearLayout layout = view.findViewById(R.id.keyBoard);

        one.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "1");
                setTextSelection();
            }
        });

        two.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "2");
                setTextSelection();
            }
        });
        three.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "3");
                setTextSelection();
            }
        });
        four.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "4");
                setTextSelection();
            }
        });
        five.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "5");
                setTextSelection();
            }
        });
        six.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "6");
                setTextSelection();
            }
        });
        seven.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "7");
                setTextSelection();
            }
        });
        eight.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "8");
                setTextSelection();
            }
        });
        nine.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                finalText.insert(selection, "9");
                setTextSelection();
            }
        });
        delete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int selection = editText.getSelectionEnd();
                if (finalText.length() > 0) {
                    finalText = stringToBuilder((selection == editText.length() - 1) ? finalText.substring(0, finalText.length() - 2) : finalText.substring(0, selection - 1) + finalText.substring(selection));
                    editText.setText(finalText);
                }
                editText.setSelection(selection > editText.length() - 1 ? finalText.length() : selection - 1 <= 1 ? 1 : selection - 1);
            }
        });
        editText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                layout.setVisibility(View.VISIBLE);
            }
        });
    }

    private StringBuilder stringToBuilder(String s) { return new StringBuilder(s); }

    private void setTextSelection() {
        int selection = editText.getSelectionEnd();
        editText.setText(finalText);
        editText.setSelection(selection + 1);
    }

}

Теперь подробно рассмотрим этот код. В сам конструктор мы передали editText и view для того, чтобы взять кнопки и назначить им выполнение ввода. Хочется отметить, что в методе кнопки delete используется «синтаксический сахар», иными словами — это сокращённая запись кода. Не всем известна эта конструкция, поэтому я решил что надо обратить на неё внимание. Особенно это может пригодится новичкам.

Работает эта конструкция таким образом

int p = (условие) ? вариант 1 : вариант 2;
int p = k == 2 ? 7 : 3;
// это можно записать и так
if (k == 2)
    p = 7;
else 
    p = 3;

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

package keyboard.develop.keyboardtest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;


public class MainActivity extends AppCompatActivity {
    private LinearLayout layout;
    private ExecutorThread executorThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); // Убираем системную клавиатуру, при этом оставляем курсор
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final EditText editText = findViewById(R.id.edit);
        layout = findViewById(R.id.keyBoard);
        new KeyBoardListener(layout, editText);
        executorThread = new ExecutorThread((TextView)findViewById(R.id.textView));
        editText.addTextChangedListener(new TextWatcher() {
            @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
            @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
            @Override
            public void afterTextChanged(Editable editable) {
                if (!editable.toString().equals("")) {
                    executorThread.setK(Integer.parseInt(editText.getText().toString()));
                    executorThread.doWork();
                }
            }
        });
    }

    @Override
    public void onBackPressed() {
        layout.setVisibility(View.GONE); // Клавиатура исчезает
    }
}


Стоит обратить внимание на эту строку

getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);

В этой строке мы отключаем системную клавиатуру, но при этом оставляем курсор, чтобы мы могли передвигаться между символами и вставлять цифры/буквы и т.д. Как раз именно поэтому мы в коде выше использовали класс StringBuilder и метод insert.
За счёт того, что весь этот метод вызывается в виде отдельного класса, мы можем добавить его куда-угодно и использовать в любых программах. Таким образом получается объектность кода.

Но я не показал вам, как сделать это в xml коде, как нам указать расположение этой клавиатуры, а всё очень просто

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="keyboard.develop.keyboardtest.MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:textColor="@color/colorPrimary"
            android:gravity="center"
            android:textSize="25sp"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="Write your text here!"/>
        <EditText
            android:id="@+id/edit"
            android:layout_width="match_parent"
            android:layout_height="50dp"/>
    </LinearLayout>

    <LinearLayout // Вот тут и располагается наша клавиатура
        android:visibility="gone"
        android:id="@+id/keyBoard"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_gravity="bottom">
        <include layout="@layout/keyboard"/> // А точнее тут, в этой строчке
    </LinearLayout>

</FrameLayout>

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

В нашем же приложении клавиатура выглядит так



Многопоточность


Многопотчность — понятно из название, что это много потоков. А много потоков — это значит выполнение нескольких операций одновременно. Многопоточность — это довольно проблемная тема в программировании. Что в C++, что в Java, что в других языках с многопоточностью всегда были проблемы. Благо почти во всех языках есть высокоуровневое решение этой проблемы. Но мы сейчас занимаемся разработкой под Android, поэтому про Anroid, а точнее про язык программирования Java мы будем говорить.

В ЯП Java есть такая вещь как Thread — она удобна при рисовании, при частой мгновенной перерисовки изображения, есть много статей на эту тему, в том числе и на Хабре, поэтому я не буду рассматривать этот вариант. Меня интересует так называемый — «засыпающий поток», поток который ждёт своего вызова для решение какой-либо поставленной задачи. Это тот случай, когда ты вызвал поток, он отработал и уснул, не тратя при этом ресурсов устройства во время ожидания новой задачи.

И название тому, что я описал выше — это класс из стандартного пакета java.util.concurrent ExecutorServise

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

Для этого создадим новый класс, который будет называться ExecutorThread. Поставим нам задачу, что нам надо прибавлять к цифре p единицу, пока это p не будет равно введённое нами числу в 4 степени. Всё делается для того, чтобы вычисление было более долгое. И чтобы весь интерфейс не зависал, мы всё вынесем это в отдельный поток.

package keyboard.develop.keyboardtest;

import android.widget.TextView;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorThread {
    private ExecutorService executorService;
    private Thread thread;
    private int p = 0;
    private int pow = 0;
    private int k = 0;
    private TextView textView;
    ExecutorThread(final TextView text) {
        p = 0;
        textView = text;
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < pow; i++)
                    p++;
                String s = "Result " + k + "^4 = " + String.valueOf(p);
                textView.setText(s);
            }
        });
        thread.start();
        executorService = Executors.newSingleThreadExecutor();
    }

    void doWork() {
        executorService.submit(thread);
    }

    void setK(int k) {
        p = 0;
        this.k = k;
        pow = (int) Math.pow(k, 4);
        textView.setText("Please wait. We are calcing!");
    }
}

Как мы видим, в тот момент пока мы не посчитали ещё, то у нас виднеется запись «Please wait. We are calcing!», что ясно — «Подождите пожалуйста. Мы считаем!». И после того, как мы посчитаем, то мы выведем текст в наш textView, который мы передали в конструктор нашего класса. Для того, чтобы всё заработало мы должны в наше activity_main, которое у нас было при создании проекта, добавить textView после нашего editText.

<TextView
            android:gravity="center"
            android:textColor="@color/colorPrimary"
            android:textSize="25sp"
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="50dp"/>

И мы так же должны добавить код ниже в наш основной класс — MainActivity.

executorThread = new ExecutorThread((TextView)findViewById(R.id.textView));
        editText.addTextChangedListener(new TextWatcher() {
            @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
            @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
            @Override
            public void afterTextChanged(Editable editable) {
                if (!editable.toString().equals("")) {
                    executorThread.setK(Integer.parseInt(editText.getText().toString()));
                    executorThread.doWork();
                }
            }
        });

Стоит отметить, что метод addTextChangeListener отвечает за то, что текст в нашем editText меняется. Как мы видим, внтури этого метода мы вызываем функцию doWork(), которая в свою очередь выполняет вот эти строки

executorService.submit(thread);


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

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

И теперь пожалуй перейдём к 3 пункту нашей статьи, а именно к интеграции рекламы.

Интеграция рекламы


На самом деле по этому поводу я не могу много рассказать, я могу только посоветовать. С рекламой не всё так легко и прозрачно. Лично я бы посоветовал использовать вам Appodeal, он не так давно на рынке, но довольно успешно работает.

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

Хочу сразу сказать, если вы вдруг будете ею пользоваться, обязательно настройте оплату в AdMob и в Appodeal, иначе реклама просто не будет грузится. Из-за того, что я не настроил счета я впустую потратил целый день, и потом мне в поддержке сказали: «А настроил ли я счета?». И после того, как я это сделал, то реклама спустя 2 часа появилась.

Заключение

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

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

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


  1. nafgne
    12.10.2018 18:45
    +2

    2010 вернулся!


  1. ruslanys
    12.10.2018 18:46

    Сделайте, пожалуйста, cut перед разделом «Кастомная клавиатура под Android».
    И вынесите листинг под спойлеры.

    Спасибо.


  1. thelongrunsmoke
    12.10.2018 18:46
    +2

    Отличный пример, как делать НЕ надо. Если вам нужна клавиатура в приложении, соизвольте генерировать её, а не описывать каждую кнопку руками.


  1. ookami_kb
    12.10.2018 18:53

    "программный песок" – это творческое переосмысление синтаксического сахара?


    1. firedragon
      12.10.2018 22:11

      Тернарный оператор, он вроде везде так зовется. Мне тоже глаза резануло.


    1. maxzh83
      13.10.2018 10:31

      Это код пожилого программиста


  1. georgeci
    12.10.2018 18:56
    +2

    Позже я соберу zip-архив нашего примера и закину куда-нибудь на диск. И оставлю потом ссылку здесь на скачивание.

    Тут стало очень толсто :)


  1. denis-19
    12.10.2018 19:20

    <cut/> что не вставили то в тексте статьи после первого абзаца?


  1. DZVang
    12.10.2018 19:28
    +1

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


  1. agent10
    12.10.2018 23:49
    +1

    Простите, но в примере с ExecutorThread что-то явно не то. Вы передаёте туда TextView, и делаете setText не из main потока. У вас точно всё работает и не падает?:)


  1. vyndor
    13.10.2018 10:34
    +1

    Это не статья для начинающих, автор сам только начал писать под Android. Неокрепшим умам может быть очень вредно.

    1. KeyBoardListener — гвоздями прибит к MainActivity, это какой-то жуткий анти-паттерн. Сама клавиатура должна предоставлять интерфейс, позволяющий подписаться на нажатие клавиш. А тут есть Listener, который знает о том как клавиатура внутри устроена, editText сам обновляет и MainActivity в конструктор принимает.

    2.

    «А почему мы не сделаем hide или не вызовем метод gone?» А я вам отвечу, это связано с жадным сборщиком мусора android. Если мы вызовем этот метод, то все указатели на кнопки пропадут, и все отклики не будет работать.
    Видимо имеется в виду setVisibility у View. Конечно никакой «жадный» GC ничего не будет собирать, на все вьюхи останутся жесткие ссылки.

    3. То что Вы назвали «библиотека в Android Java ExecutorServise» никакая не библиотека, а просто класс из стандартного пакета java.util.concurrent.

    4. ExecutorThread прекрасен во всех отношенях.
    • Принимает TextView, что черевато утечками и крашами. Activity уже давно умрёт, а поток из executor'а будет всё ещё держать ссылку на TextView, а значит и на всю activity + будет ещё и пытаться что-то обновить на уже «мертвой» activity.
    • doWork каждый раз вызывает newSingleThreadExecutor, что убивает всю идею executor'ов.


    Это прям беглым взлядом и про совсем ахинею.

    Автор пару советов:
    1. Сделайте клавиатуру отдельной вьюхой.
    2. Почитатйте про Executor'ы, у вас нет понимания зачем они вообще нужны.


    1. Develop9999 Автор
      14.10.2018 14:40

      Спасибо, большую часть ошибок поправил. TextView — не падает, я проверял. Согласен, жуткий анти-паттерн. Поправил. Мог бы улучшить, если бы знал, как вызвать onBackPressed() внутри класса. И по поводу setVisible(), у меня действительно пропадали указатели, потом где-то вычитал, что это связано со сборщиком мусора, сейчас переделал — всё заработало