Настройка Android: как создать компонент пользовательского интерфейса, который делает то, что вы хотите

Опубликовано: 2022-03-11

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

Модель пользовательского интерфейса Android по своей природе настраиваема, предлагая средства настройки Android, тестирования и возможность создавать пользовательские компоненты пользовательского интерфейса различными способами:

  • Наследовать существующий компонент (например, TextView , ImageView и т. д.) и добавлять/переопределять необходимые функции. Например, CircleImageView , который наследует ImageView , переопределяет onDraw() , чтобы ограничить отображаемое изображение кругом, и добавляет функцию loadFromFile() для загрузки изображения из внешней памяти.

  • Создайте составной компонент из нескольких компонентов. Этот подход обычно использует макеты для управления расположением компонентов на экране. Например, LabeledEditText , который наследует LinearLayout с горизонтальной ориентацией и содержит TextView , действующий как метка, и EditText , действующий как поле ввода текста.

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

  • Самый универсальный и самый сложный подход — создать самостоятельно нарисованный компонент . В этом случае компонент наследует общий класс View и переопределяет такие функции, как onMeasure() для определения его макета, onDraw() для отображения его содержимого и т. д. Компоненты, созданные таким образом, обычно сильно зависят от API 2D-рисования Android.

Пример настройки Android: CalendarView

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

Например, компонент CalendarView не позволяет изменить способ маркировки определенного дня или используемый цвет фона. Также нет возможности добавить какой-либо пользовательский текст или графику, например, чтобы отметить особый случай. Вкратце компонент выглядит так, и почти ничего нельзя изменить:

Снимок экрана
CalendarView в теме AppCompact.Light .

Сделай свой собственный

Итак, как же создать собственное представление календаря? Любой из вышеперечисленных подходов будет работать. Однако практичность обычно исключает третий вариант (2D-графика) и оставляет нам два других метода, и в этой статье мы будем использовать их сочетание.

Чтобы продолжить, вы можете найти исходный код здесь.

1. Компоновка компонентов

Во-первых, давайте начнем с того, как выглядит компонент. Для простоты давайте отобразим дни в сетке, а вверху — название месяца вместе с кнопками «следующий месяц» и «предыдущий месяц».

Снимок экрана
Пользовательский вид календаря.

Этот макет определен в файле control_calendar.xml следующим образом. Обратите внимание, что некоторая повторяющаяся разметка была сокращена с помощью ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. Класс компонента

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

Наш компонент пользовательского интерфейса будет LinearLayout , чтобы соответствовать корню файла макета XML. Обратите внимание, что из кода показаны только важные части. Реализация компонента находится в CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

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

3. Нужна логика

Чтобы заставить этот компонент вести себя как представление календаря, необходима некоторая бизнес-логика. Сначала это может показаться сложным, но на самом деле в этом нет ничего сложного. Давайте разберем это:

  1. Представление календаря имеет ширину семь дней, и гарантируется, что все месяцы будут начинаться где-то в первой строке.

  2. Во-первых, нам нужно выяснить, с какой позиции начинается месяц, затем заполнить все позиции до этого числами из предыдущего месяца (30, 29, 28 и т. д.), пока мы не достигнем позиции 0.

  3. Затем мы заполняем дни текущего месяца (1, 2, 3… и т. д.).

  4. После этого идут дни следующего месяца (снова 1, 2, 3 и т. д.), но на этот раз мы заполняем только оставшиеся позиции в последних строках сетки.

Следующая диаграмма иллюстрирует эти шаги:

Снимок экрана
Пользовательская бизнес-логика представления календаря.

Ширина сетки уже указана и составляет семь ячеек, обозначающих недельный календарь, но как насчет высоты? Наибольший размер сетки можно определить по наихудшему сценарию 31-дневного месяца, начинающегося в субботу, которая является последней ячейкой в ​​первой строке, и для полного отображения потребуется еще 5 строк. Таким образом, настройки календаря для отображения шести строк (всего 42 дня) будет достаточно для обработки всех случаев.

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

Как упоминалось ранее, функциональные возможности даты, предоставляемые классом Calendar , делают реализацию довольно простой. В нашем компоненте updateCalendar() реализует такую ​​логику:

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. Настраиваемость в глубине души

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

Для этого примера нам потребуется следующее от нашего CalendearView :

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

Первые три требования легко выполнить, изменив атрибуты текста и фоновые ресурсы. Давайте реализуем CalendarAdapter для выполнения этой задачи. Он достаточно прост, чтобы быть классом-членом в CalendarView . Переопределив getView() , мы можем выполнить указанные выше требования:

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

Окончательное требование к дизайну требует немного больше работы. Во-первых, давайте добавим цвета для четырех сезонов в /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

Затем давайте воспользуемся массивом для определения времени года для каждого месяца (для простоты предположим, что это северное полушарие; простите, Австралия!). В CalendarView мы добавляем следующие переменные-члены:

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

Таким образом, выбор соответствующего цвета осуществляется путем выбора соответствующего сезона ( monthSeason[currentMonth] ), а затем выбора соответствующего цвета ( rainbow[monthSeason[currentMonth] ), который добавляется в updateCalendar() , чтобы убедиться, что выбран соответствующий цвет. при каждом изменении календаря.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

При этом мы получаем следующий результат:

Настройка Android
Цвет заголовка меняется в зависимости от сезона.

Важное примечание : из-за того, как HashSet сравнивает объекты, приведенная выше проверка eventDays.contains(date) в updateCalendar() не даст истинного значения для объектов даты, если только они не идентичны. Он не выполняет никаких специальных проверок для типа данных Date . Чтобы обойти это, эта проверка заменяется следующим кодом:

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. Во время разработки это выглядит некрасиво

Выбор Android для заполнителей во время разработки может быть сомнительным. К счастью, Android фактически создает экземпляр нашего компонента, чтобы отобразить его в дизайнере пользовательского интерфейса, и мы можем воспользоваться этим, вызвав updateCalendar() в конструкторе компонента. Таким образом, компонент действительно будет иметь смысл во время разработки.

Снимок экрана

Если инициализация компонента требует большого объема обработки или загрузки большого количества данных, это может повлиять на производительность среды IDE. В этом случае Android предоставляет изящную функцию isInEditMode() , которую можно использовать для ограничения объема данных, используемых при фактическом создании экземпляра компонента в дизайнере пользовательского интерфейса. Например, если в CalendarView нужно загрузить много событий, мы можем использовать isInEditMode() внутри функции updateCalendar() , чтобы предоставить пустой/ограниченный список событий в режиме разработки, а в противном случае загрузить настоящий.

6. Вызов компонента

Компонент может быть включен в XML-файлы макета (пример использования можно найти в файле activity_main.xml ):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

И извлекается для взаимодействия после загрузки макета:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

Приведенный выше код создает HashSet событий, добавляет к нему текущий день, а затем передает его в CalendarView . В результате CalendarView отобразит текущий день жирным синим цветом, а также поместит на него маркер события:

Снимок экрана
CalendarView , отображающий событие

7. Добавление атрибутов

Другая возможность, предоставляемая Android, — назначение атрибутов пользовательскому компоненту. Это позволяет разработчикам Android, использующим компонент, выбирать настройки с помощью XML-макета и сразу же видеть результат в дизайнере пользовательского интерфейса, вместо того, чтобы ждать и смотреть, как CalendarView выглядит во время выполнения. Добавим возможность изменять формат отображения даты в компоненте, например указывать полное название месяца вместо трехбуквенной аббревиатуры.

Для этого необходимы следующие шаги:

  • Объявите атрибут. Назовем его dateFormat и присвоим string тип данных. Добавьте его в /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Используйте атрибут в макете, который использует компонент, и присвойте ему значение "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • Наконец, пусть компонент использует значение атрибута:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

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

Снимок экрана
Изменение атрибутов CalendarView .

8. Взаимодействие с компонентом

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

Ответ состоит из двух частей:

  • Захват событий внутри компонента и
  • Сообщать о событиях родительскому компоненту (это может быть Fragment , Activity или даже другой компонент).

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

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

Существует несколько способов сообщения о событиях. Прямой и простой способ — скопировать то, как это делает Android: он предоставляет интерфейс для событий компонента, который реализуется родителем компонента ( eventHandler в приведенном выше фрагменте кода).

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

 public interface EventHandler { void onDayLongPress(Date date); }

Реализация, предоставленная родителем, может быть передана в представление календаря через setEventHandler() . Вот пример использования из `MainActivity.java':

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

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

Снимок экрана

Другой, более продвинутый способ справиться с этим — использовать Android Intents и BroadcastReceivers . Это особенно полезно, когда несколько компонентов должны быть уведомлены о событии календаря. Например, если для нажатия дня в календаре требуется, чтобы текст отображался в Activity , а файл загружался фоновой Service .

Использование предыдущего подхода потребует, чтобы Activity предоставило компоненту EventHandler , обработав событие и затем передав его в Service . Вместо этого, если компонент передает Intent , а Activity и Service принимают его через свои собственные BroadcastReceivers , это не только упрощает жизнь, но и помогает отделить рассматриваемую Activity от Service .

Заключение

Оцените потрясающую мощь персонализации Android!
Твитнуть

Итак, вот как вы можете создать свой собственный компонент за несколько простых шагов:

  • Создайте макет XML и настройте его в соответствии с вашими потребностями.
  • Получите класс вашего компонента от соответствующего родительского компонента в соответствии с макетом XML.
  • Добавьте бизнес-логику вашего компонента.
  • Используйте атрибуты, чтобы пользователи могли изменять поведение компонента.
  • Чтобы упростить использование компонента в дизайнере пользовательского интерфейса, используйте функцию Android isInEditMode() .

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

Спасибо за чтение этого руководства, я желаю вам удачи в ваших усилиях по кодированию!