Статьи

Пользовательский интерфейс Android: полное приложение

Эта статья является частью нашего Академического курса под названием Android UI Design — Основы .

В этом курсе вы познакомитесь с основами дизайна пользовательского интерфейса Android. Вы поймете пользовательский ввод, представления и макеты, а также адаптеры и фрагменты. Проверьте это здесь !

1. Введение

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

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

2. Структура приложения

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

Возвращаясь к нашему приложению To do, мы можем представить, что у нас есть следующие требования:

  • Там должен быть список предметов (для выполнения предметов).
  • Пользователь может добавить элемент к существующим.
  • Предметы должны иметь приоритетный цвет.
  • Приложение должно работать на смартфоне и планшете.

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

фигура 1

фигура 1

фигура 2

фигура 2

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

Теперь, когда мы примерно знаем, какой будет навигация и как будет выглядеть пользовательский интерфейс приложения, мы можем приступить к созданию нашего приложения с помощью нашей IDE. В этом случае мы будем использовать Eclipse + ADT. Мы создаем новый проект Android, который мы можем назвать Todo . Мы не будем рассказывать, как создать проект Android с использованием Eclipse, поэтому мы предполагаем, что вы уже знакомы с этой IDE. Посмотрите наш пример «Android Hello World», если вы не знакомы с процессом.

2.1. Список предметов с ListView и объектной моделью

Теперь у нас есть структура проекта, и мы можем сосредоточиться на разработке модели, стоящей за приложением. В этом случае модель очень проста, это просто класс, который содержит информацию о новом элементе todo:

1
2
3
4
5
6
7
8
public class Item implements Serializable {
    private String name;   
    private String descr;
    private Date date;
    private String note;
    private TagEnum tag;
            // Set and get methods
}

Это будет основной класс, который мы будем обрабатывать в нашем приложении. Глядя на проект Android, который мы только что создали, мы можем заметить, что под каталогом макетов есть макет по умолчанию, называемый activity_main.xml . Это макет по умолчанию, созданный инструментом.

К настоящему времени мы можем предположить, что у нас есть только список элементов в этом макете: этот макет будет использоваться только для смартфона, и мы рассмотрим позже, когда приложение будет работать на планшете. Расположение элемента списка очень простое, оно просто создается стандартным виджетом ListView :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >
 
    <ListView
        android:id="@+id/listItmes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
 
</RelativeLayout>

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

Рисунок 3

Рисунок 3

Как вы можете заметить, каждая строка имеет изображение слева, которое представляет приоритет задачи и некоторую информацию. В настоящее время мы не рассматриваем применение какого-либо стиля к нашим строкам. Чтобы иметь такую ​​строку в нашем ListView , мы должны создать макет строки, который мы будем использовать в нашем настраиваемом адаптере. Таким образом, мы можем создать новый файл с именем item_layout.xml в директории layout . Этот файл выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <ImageView
        android:id="@+id/tagView"
        android:layout_width="30dp"
        android:layout_height="20dp"
        android:background="@color/red" />
 
    <TextView
        android:id="@+id/nameView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@id/tagView" />
 
    <TextView
        android:id="@+id/descrView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/nameView"
        android:layout_toRightOf="@id/tagView" />
 
    <TextView
        android:id="@+id/dateView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true" />
 
</RelativeLayout>

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

1
android:layout_toRightOf="@id/tagView"

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

1
2
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"

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

1
2
3
4
5
6
7
8
9
public class ToDoItemAdapter extends ArrayAdapter<Item> {
    private Context ctx;
    private List<Item> itemList;
    public ToDoItemAdapter(Context context, List<Item> itemList) {
        super(context, R.layout.item_layout);
        this.ctx = context;
        this.itemList = itemList;
    }
}

Конструктор получает Context и itemList качестве параметров, последний параметр содержит список элементов todo. Вы можете заметить, что когда мы вызываем супер метод, мы R.layout.item_layout пользовательский макет с именем R.layout.item_layout . Теперь нам нужно переопределить один из наиболее важных методов, называемый getView , который используется для создания View и рендеринга макета строки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    ItemHolder h = null;
    if (v == null) {
        // Inflate row layout
        LayoutInflater inf = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        v = inf.inflate(R.layout.item_layout, parent, false);
        // Look for Views in the layout
        ImageView iv = (ImageView) v.findViewById(R.id.tagView);
        TextView nameTv = (TextView) v.findViewById(R.id.nameView);
        TextView descrView = (TextView) v.findViewById(R.id.descrView);
        TextView dateView = (TextView) v.findViewById(R.id.dateView);
        h = new ItemHolder();
        h.tagView = iv;
        h.nameView = nameTv;
        h.descrView = descrView;
        h.dateView = dateView;
        v.setTag(h);
    }
    else       
       h = (ItemHolder) v.getTag();
 
    h.nameView.setText(itemList.get(position).getName());      
    h.descrView.setText(itemList.get(position).getDescr());
    h.tagView.setBackgroundResource(itemList.get(position).getTag().getTagColor());
    h.dateView.setText(sdf.format(itemList.get(position).getDate()));
     
    return v;
}

В этом методе мы проверяем в начале, является ли представление, которое мы получаем как параметр, нулевым. В этом случае мы должны надуть наш макет. Если вы заметили, мы использовали шаблон ViewHolder , чтобы сделать прокрутку ListView более плавной. Мы создали небольшой внутренний класс ItemHolder который содержит ссылки на View внутри нашего пользовательского макета:

1
2
3
4
5
6
7
// ViewHolder pattern
static class ItemHolder {
    ImageView tagView;
    TextView nameView;
    TextView descrView;
    TextView dateView;     
}

Одна вещь, которую вы должны заметить, это то, как мы обрабатывали цвет фона ImageView . Мы использовали setBackgroundResource для установки imageview изображения. Этот метод принимает int, представляющий идентификатор ресурса, который мы хотим использовать в качестве фона:

1
h.tagView.setBackgroundResource(itemList.get(position).getTag().getTagColor());

Глядя на наш класс модели, мы можем заметить, что метод getTag() возвращает экземпляр класса TagEnum . Это перечисление, определенное следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
public enum TagEnum {
    BLACK(R.color.black,"Black"), RED(R.color.red, "Red"),
    GREEN(R.color.green, "Green"), BLUE(R.color.blue, "Blue"),YELLOW(R.color.yellow,"Yellow");
    private int code;
    private String name;
    private TagEnum(int code, String name) {
        this.code = code;
        this.name = name;
    }
    public int getTagColor() {
        return this.code;
    }
}

В перечислении мы определяем различные цвета, которые мы хотим поддерживать, и в качестве первого параметра мы передаем идентификатор ресурса. Если вы помните, в предыдущей статье мы говорили о том, как определить цвет ресурса в формате XML. Мы уже знаем, что нам нужно создать XML-файл в res/values который мы можем назвать colors.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<resources>
 
    <color name="red" >#FF0000
    </color>
 
    <color name="green" >#00FF00
    </color>
 
    <color name="blue" >#0000FF
    </color>
 
    <color name="black" >#000000
    </color>
 
    <color name="yellow" >#FFAA00
    </color>
 
</resources>

В определении цвета перечисления мы ссылались на этот цвет, используя R.color.color_name , поэтому, когда мы используем метод getTagColor в пользовательском адаптере getView , мы получаем идентификатор ресурса, который будет использоваться фоном изображения. Важно понимать, что мы не жестко кодировали цвета в конструкторе: например, мы могли напрямую использовать шестнадцатеричный код цвета, например, # FF0000 для красного и так далее.

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

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

2.2. Поддержка нескольких устройств и особенности компоновки

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

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

Рисунок 4

Рисунок 4

Рисунок 5

Рисунок 5

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

Мы можем предположить, что размер экрана составляет не менее 600 dp, поэтому мы хотим разделить экран на другие области и определить новый макет в res/layout-sw600dp :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >
 
    <ListView
        android:id="@+id/listItmes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1" />
 
    <FrameLayout
        android:id="@+id/frm1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1" />
 
</LinearLayout>

где FrameLayout будет «заполняться» динамически в зависимости от потока навигации.

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

2,3. Добавить элемент макета пользовательского интерфейса

Если мы запустим приложение, то получим пустой список без элементов. Мы должны создать новый пользовательский интерфейс. Поэтому, учитывая соображения, которые мы делали ранее, мы создаем Fragment который обрабатывает функциональность добавления элемента, называя его NewItemFragment . Фрагмент имеет сложный жизненный цикл, но для этого мы можем переопределить только метод onCreateView . Этот метод отвечает за создание пользовательского интерфейса. Как всегда, мы должны сначала создать макет. В нашей IDE в res/layout мы создаем еще один XML-файл, который мы называем add_item_layout.xml :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <TextView
        android:id="@+id/txtTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/addItem" />
 
    <TextView
        android:id="@+id/itemName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/txtTitle"
        android:layout_marginStart="10dp"
        android:text="@string/addItemName" />
 
    <EditText
        android:id="@+id/edtName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/itemName"
        android:hint="@string/addItemNameHint" />
 
    <TextView
        android:id="@+id/itemDescr"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/edtName"
        android:layout_marginTop="10dp"
        android:text="@string/addItemDescr" />
 
    <EditText
        android:id="@+id/edtDescr"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/itemDescr"
        android:hint="@string/addItemDescrHint" />
 
    <TextView
        android:id="@+id/itemNote"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/edtDescr"
        android:layout_marginTop="10dp"
        android:text="@string/addItemNote" />
 
    <EditText
        android:id="@+id/edtNote"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/itemNote"
        android:hint="@string/addItemNoteHint" />
 
    <TextView
        android:id="@+id/itemDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/edtNote"
        android:layout_marginTop="10dp"
        android:text="@string/addItemDate" />
 
    <TextView
        android:id="@+id/inDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/itemDate"
        android:layout_marginTop="10dp" />
 
    <TextView
        android:id="@+id/itemTime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/inDate"
        android:layout_marginTop="10dp"
        android:text="@string/addItemTime" />
 
    <TextView
        android:id="@+id/inTime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/itemTime"
        android:layout_marginTop="10dp" />
 
    <TextView
        android:id="@+id/itemTag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/inTime"
        android:layout_marginTop="10dp"
        android:text="@string/addItemTag" />
 
    <Spinner
        android:id="@+id/tagSpinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/itemTag" />
 
    <!-- ADD button -->
 
    <Button
        android:id="@+id/addBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="@string/addButton" />
 
</RelativeLayout>

Это очень простой макет, созданный TextView и EditText : первый используется для отображения текстовых сообщений в пользовательском интерфейсе, а другой — для приема пользовательских данных. В этом макете важны два компонента: Spinner и Spinner Date/Time picker .

Спиннер — это компонент пользовательского интерфейса, который показывает только один элемент за раз и позволяет пользователю выбрать один элемент среди них. Мы используем этот компонент, чтобы показать различные цвета тегов / приоритеты. Это идеально подходит для нашей цели, на самом деле мы хотим, чтобы пользователь выбирал один цвет из списка цветов.

2,4. Цвет метки / приоритетный спиннер

Для правильной работы Spinner необходим адаптер массива. Android предоставляет список адаптеров, которые мы можем использовать, но мы хотим настроить их, потому что мы хотим показать изображение с цветом. Затем мы должны создать пользовательский адаптер так же, как мы делали для ListView . Сначала мы создаем макет строки с именем spinner_tag_layout.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <ImageView
        android:id="@+id/tagSpinnerImage"
        android:layout_width="30dp"
        android:layout_height="20dp" />
 
    <TextView
        android:id="@+id/tagNameSpinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/tagSpinnerImage" />
 
</RelativeLayout>

и наконец мы создаем наш адаптер:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TagSpinnerAdapter extends ArrayAdapter<TagEnum> {
 
    private Context ctx;
    private List<TagEnum> tagList;
     
    public TagSpinnerAdapter(Context ctx, List<TagEnum> tagList) {
        super(ctx, R.layout.spinner_tag_layout);
        this.ctx = ctx;
        this.tagList = tagList;
    }
 
     
    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return _getView(position, convertView, parent);
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return _getView(position, convertView, parent);
    }
 
    private View _getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;
        if (v == null) {
            // Inflate spinner layout
            LayoutInflater inf = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = inf.inflate(R.layout.spinner_tag_layout, parent, false);
        }
         
        // We should use ViewHolder pattern
        ImageView iv = (ImageView) v.findViewById(R.id.tagSpinnerImage);
        TextView tv = (TextView) v.findViewById(R.id.tagNameSpinner);
         
        TagEnum t = tagList.get(position);
        iv.setBackgroundResource(t.getTagColor());
        tv.setText(t.getName());
        return v;
    }
 
}

Анализируя приведенный выше код, мы замечаем, что этот адаптер обрабатывает объекты TagEnum и мы переопределяем два метода getView и getDropDownView . Мы обрабатываем эти методы одинаково. Как вы можете заметить, мы сделали почти то же самое, что уже сделали для ListView.

Во фрагменте, который содержит эти компоненты пользовательского интерфейса, мы должны найти ссылку на Spinner и установить пользовательский макет, который мы определили выше:

1
2
3
Spinner sp = (Spinner) v.findViewById(R.id.tagSpinner);
TagSpinnerAdapter tsa = new TagSpinnerAdapter(getActivity(), tagList);
sp.setAdapter(tsa);

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
sp.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> adptView, View view,
        int pos, long id) {
        currentTag = (TagEnum) adptView.getItemAtPosition(pos);
    }
 
    @Override
    public void onNothingSelected(AdapterView<?> arg0) {
        // TODO Auto-generated method stub
                 
        }
    });

и мы храним результат в атрибуте класса.

2.5. Выбор даты и времени

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

Мы используем фрагменты, поэтому нам нужно создать два внутренних класса, которые представляют пользователю указатели даты и времени. В этом случае мы расширяем для обоих сборщиков класс DialogFragment и переопределяем метод onCreateDialog . В этом методе мы просто инициализируем средство выбора Date и возвращаем его как результат:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class DatePickerFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener {
    @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            // Use the current date as the default date in the picker
            final Calendar c = Calendar.getInstance();
            c.setTime(selDate);
            int year = c.get(Calendar.YEAR);
            int month = c.get(Calendar.MONTH);
            int day = c.get(Calendar.DAY_OF_MONTH);
 
            // Create a new instance of DatePickerDialog and return it
            return new DatePickerDialog(getActivity(), this, year, month, day);
        }
         
    @Override
    public void onDateSet(DatePicker view, int year, int monthOfYear,
                int dayOfMonth) {
             
        Calendar c = Calendar.getInstance();
        c.set(year, monthOfYear, dayOfMonth, 9, 0);
        selDate = c.getTime();
        tvDate.setText(sdfDate.format(selDate));
    }
}

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

Таким же образом мы обрабатываем TimePickerFragment , но в этом случае мы расширяем TimePickerDialog :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class TimePickerFragment extends DialogFragment implements TimePickerDialog.OnTimeSetListener {
    @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
            // Use the current date as the default date in the picker
            final Calendar c = Calendar.getInstance();
            c.setTime(selDate);
            int hour = c.get(Calendar.HOUR_OF_DAY);
            int minute = c.get(Calendar.MINUTE);
        // Create a new instance of TimePickerDialog and return it
         return new TimePickerDialog(getActivity(), this, hour, minute,
                        DateFormat.is24HourFormat(getActivity()));
            }
 
          @Override
          public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
        Calendar c = Calendar.getInstance();
        c.setTime(selDate);
        c.set(Calendar.HOUR_OF_DAY, hourOfDay);
        c.set(Calendar.MINUTE, minute);
        selDate = c.getTime();
        // We set the hour
        tvTime.setText(sdfTime.format(selDate));
    }
}

Это два диалога, которые не отображаются сами по себе, но мы должны проверить, нажимает ли пользователь на текстовое представление даты / времени, чтобы открыть эти диалоги, поэтому мы имеем:

1
2
tvDate = (TextView) v.findViewById(R.id.inDate);
tvTime = (TextView) v.findViewById(R.id.inTime);

чтобы получить ссылку на TextView а затем мы просто реализуем слушатель:

1
2
3
4
5
6
7
tvDate.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        DatePickerFragment dpf = new DatePickerFragment();
        dpf.show(getFragmentManager(), "datepicker");
    }
});

В результате мы имеем:

Рисунок 6

Рисунок 6

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

Мы можем определить этот интерфейс в классе фрагмента:

1
2
3
public interface AddItemListener {
    public void onAddItem(Item item);
}

Это очень простой интерфейс, созданный только одним методом. Теперь у нас есть:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
Button addBtn = (Button) v.findViewById(R.id.addBtn);
addBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // We retrieve data inserted
        Item i = new Item();
        i.setName(edtName.getText().toString());
        i.setDescr(edtDescr.getText().toString());
        i.setNote(edtNote.getText().toString());
        i.setTag(currentTag);
        i.setDate(selDate);
        // Safe cast
        ( (AddItemListener) getActivity()).onAddItem(i);
    }
});

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class NewItemActivity extends Activity implements NewItemFragment.AddItemListener{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        NewItemFragment nif = new NewItemFragment();
        getFragmentManager().beginTransaction().add(android.R.id.content, nif).commit();
    }
 
    @Override
    public void onAddItem(Item item) {
        // We get the item and return to the main activity
        Log.d("TODO", "onAddItem");
        Intent i = new Intent();
        i.putExtra("item", item);
        setResult(RESULT_OK,i);
        finish();
    }
}

В методе onCreate мы создаем новый экземпляр нашего класса фрагмента и, используя FragmentManager показываем фрагмент на экране.

Обратите внимание, что в этом действии реализован интерфейс, определенный во фрагменте, потому что он должен быть уведомлен, когда пользователь хочет добавить новый элемент, поэтому он реализует метод onAddItem . Мы расскажем о функциональности этого метода позже.

2.6. Основное занятие

Основное действие — это сердце приложения, это действие, которое вызывается при запуске. Он устанавливает начальный макет и показывает список элементов, который мы ранее описали:

1
2
3
itemListView = (ListView) findViewById(R.id.listItmes);
adpt = new ToDoItemAdapter(this, itemList);
itemListView.setAdapter(adpt);

Теперь мы должны проверить, работает ли наше приложение на смартфоне или планшете, чтобы мы могли изменить поведение активности. Мы можем сделать это, проверив, есть FrameLayout компонент FrameLayout в определении макета:

1
2
if (findViewById(R.id.frm1) != null)
   isTablet = true;

2,7. Панель действий

В этом упражнении мы добавляем панель действий (известный шаблон Android) с действием: добавить новый элемент. Чтобы определить его, мы создаем (если не существует) файл XML в res/menu :

1
2
3
4
5
6
7
8
9
 
    <item
        android:id="@+id/action_add"
        android:icon="@android:drawable/ic_menu_add"
        android:orderInCategory="0"
        android:showAsAction="always"/>
 
</menu>

В результате получаем:

Рисунок 7

Рисунок 7

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int menuId = item.getItemId();
    switch (menuId) {
        case R.id.action_add: {
            if (!isTablet) {
                Intent i = new Intent(this, NewItemActivity.class);
                startActivityForResult(i, ADD_ITEM);
                break;
            }
            else {
                Log.d("TODO", "Tablet");
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                NewItemFragment nif = new NewItemFragment();
                ft.replace(R.id.frm1, nif);
                ft.commit();
            }
        }
    }
}

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

Если мы запускаем действие, ожидая его результата в вызываемом NewItemActivity мы должны вернуть результат, и мы делаем это в NewItemActivity следующим образом:

1
2
3
4
5
6
7
8
9
@Override
public void onAddItem(Item item) {
    // We get the item and return to the main activity
    Log.d("TODO", "onAddItem");
    Intent i = new Intent();
    i.putExtra("item", item);
    setResult(RESULT_OK,i);
    finish();
}

В этом методе мы создаем Intent, который содержит результат, и передаем его обратно в вызывающее действие ( MainActivity ), и мы завершаем действие. В MainActivity мы должны быть готовы обработать результат:

01
02
03
04
05
06
07
08
09
10
11
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.d("TODO", "OnResult");
    if (requestCode == ADD_ITEM) {
        if (resultCode == RESULT_OK) {
            Log.d("TODO", "OK");
            Item i = (Item) data.getExtras().getSerializable("item");
            itemList.add(i);
            adpt.notifyDataSetChanged();
        }
    }
}

В этом упражнении мы извлекаем объект предмета, сохраненный в Intent которое мы получаем в результате, и добавляем его в список предметов. Когда мы закончим, мы вызываем метод notifyDataSetChange для обновления ListView .

Если наше приложение работает на планшете, мы просто «заполняем» NewItemFragment описанным выше NewItemFragment . В этом случае мы запускаем транзакцию и заменяем FrameLayout фрагментом, а в конце мы FrameLayout транзакцию. В этом случае нам не нужно запускать другое действие, потому что мы используем FrameLayout чтобы показать фрагмент, который обрабатывает пользовательский интерфейс для добавления нового элемента. Итак, мы имеем:

Рисунок 8

Рисунок 8

В этом случае мы должны просто реализовать интерфейс, указанный фрагментом, как мы делали раньше:

1
2
3
4
5
6
7
@Override
public void onAddItem(Item item) {
    itemList.add(item);
    adpt.notifyDataSetChanged();
    NewItemFragment nif = (NewItemFragment) getFragmentManager().findFragmentById(R.id.frm1);
    getFragmentManager().beginTransaction().remove(nif).commit();
}

Обратите внимание, что в последней части мы удалили фрагмент в конце.

3. Стилизация приложения

К настоящему времени мы не рассмотрели стиль приложения, но мы знаем, что можем применять стиль к каждому компоненту пользовательского интерфейса. Мы можем изменить все цвета и внешний вид пользовательского интерфейса, реализуя наш стиль и выпуская бренд приложения. Например, мы хотим применить стиль к строке listView, делая их немного более привлекательными. Мы создаем (если он еще не существует) файл с именем style.xml разделе res/values и здесь мы можем определить наш стиль, как мы обсуждали в предыдущей статье :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<style name="dateStyle">
        <item name="android:textAppearance">?android:textAppearanceSmall</item>
        <item name="android:textColor">@color/red</item>
    </style>
 
     <style name="descrStyle">
        <item name="android:textStyle">italic</item>
        <item name="android:textAppearance">?android:textAppearanceSmall</item>
    </style>
     
     <style name="nameStyle">
         <item name="android:textAppearance">?android:textAppearanceMedium</item>
         <item name="android:textStyle">bold</item>
     </style>

Мы определили три разных стиля и хотим применить их к нашей строке списка:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<TextView
    android:id="@+id/nameView"
    style="@style/nameStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_toRightOf="@id/tagView" />
<TextView
    android:id="@+id/descrView"
    style="@style/descrStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/nameView"
    android:layout_toRightOf="@id/tagView" />
<TextView
    android:id="@+id/dateView"
    style="@style/dateStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_alignParentRight="true" />

Запустив приложение с этим стилем мы имеем:

Рисунок 9

Рисунок 9

4. Вывод

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

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

5. Загрузите исходный код

Это был урок о том, как создать пользовательский интерфейс приложения Android с нуля. Вы можете скачать исходный код здесь: AndroidApp.zip