Android自定义(如何构建可以满足你需求的UI组件)

本文概述

  • Android自定义案例研究:CalendarView
  • 自己做
  • 总结
开发人员发现自己需要的UI组件并不少见, 这不是由他们所针对的平台提供的, 或者确实是由UI提供的, 但是缺少某些属性或行为。两种方案的答案都是自定义UI组件。
【Android自定义(如何构建可以满足你需求的UI组件)】Android UI模型具有固有的可自定义性, 它提供了Android自定义, 测试和通过各种方式创建自定义UI组件的功能:
  • 继承现有组件(即TextView, ImageView等), 并添加/覆盖所需的功能。例如, 一个CircleImageView继承ImageView, 重写onDraw()函数以将显示的图像限制为一个圆形, 并添加loadFromFile()函数以从外部存储器加载图像。
  • 从多个组件中创建一个复合组件。这种方法通常利用布局来控制组件在屏幕上的排列方式。例如, 一个LabeledEditText继承了水平方向的LinearLayout, 并且同时包含充当标签的TextView和充当文本输入字段的EditText。
    这种方法也可以利用前一种方法, 即内部组件可以是本地的或自定义的。
  • 最通用, 最复杂的方法是创建一个自绘组件。在这种情况下, 组件将继承通用的View类, 并覆盖诸如onMeasure()以确定其布局, onDraw()来显示其内容等功能。以这种方式创建的组件通常在很大程度上取决于Android的2D绘图API。
Android自定义案例研究:CalendarView Android提供了本机CalendarView组件。它运行良好, 并提供任何日历组件所期望的最低功能, 显示一个完整的月份并突出显示当前日期。有人可能会说它看起来也不错, 但前提是你要具有本机外观, 并且对自定义外观毫无兴趣。
例如, CalendarView组件无法更改某天的标记方式或使用的背景颜色。例如, 也无法添加任何自定义文本或图形来标记特殊情况。简而言之, 组件看起来像这样, 几乎什么都不能更改:
Android自定义(如何构建可以满足你需求的UI组件)

文章图片
AppCompact.Light主题中的CalendarView。
自己做 那么, 如何创建自己的日历视图呢?上面的任何方法都可以。但是, 实用性通常会排除第三个选项(2D图形), 而让我们剩下其他两种方法, 因此在本文中我们将两者结合使用。
接下来, 你可以在此处找到源代码。
1.组件布局
首先, 让我们从组件的外观开始。为简单起见, 让我们在网格中显示日期, 并在顶部显示月份名称以及” 下个月” 和” 上个月” 按钮。
Android自定义(如何构建可以满足你需求的UI组件)

文章图片
自定义日历视图。
此布局在文件control_calendar.xml中定义, 如下所示。请注意, 某些重复标记已缩写为… :
< ?xml version="1.0" encoding="utf-8"?> < LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 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:id="@+id/calendar_prev_button" android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="http://www.srcmini.com/@drawable/previous_icon"/> < !-- date title --> < TextView android:id="@+id/calendar_date_display" 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:id="@+id/calendar_next_button" ... Same layout as prev button. android:src="http://www.srcmini.com/@drawable/next_icon"/> < /RelativeLayout> < !-- days header --> < LinearLayout android:id="@+id/calendar_header" 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:id="@+id/calendar_grid" android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> < /LinearLayout>

2.组件类
先前的布局可以按原样包含在” 活动” 或” 片段” 中, 并且可以正常工作。但是将其封装为独立的UI组件将防止代码重复, 并允许进行模块化设计, 其中每个模块都承担一个责任。
我们的UI组件将是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. 日历视图的宽度为7天, 可以确保所有月份都从第一行的某个位置开始。
  2. 首先, 我们需要弄清该月的开始位置, 然后使用前一个月的数字(30、29、28等)填充之前的所有位置, 直到到达位置0。
  3. 然后, 我们填写当月的日期(1、2、3…等)。
  4. 在那之后是下个月的日子(再次是1、2、3等), 但是这次我们只填写网格最后一行中的剩余职位。
下图说明了这些步骤:
Android自定义(如何构建可以满足你需求的UI组件)

文章图片
自定义日历视图业务逻辑。
网格的宽度已经指定为七个单元格, 表示每周日历, 但是高度如何?网格的最大大小可以通过最坏的情况确定, 即从星期六开始的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自定义(如何构建可以满足你需求的UI组件)

文章图片
标题颜色根据季节而变化。
重要说明:由于HashSet比较对象的方式, 除非日期对象完全相同, 否则updateCalendar()中的上述check eventDays.contains(date)对于日期对象不会产生true。它不对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实际上实例化了我们的组件以便在UI设计器中呈现它, 我们可以通过在组件构造函数中调用updateCalendar()来利用它。这样, 组件将在设计时真正有意义。
Android自定义(如何构建可以满足你需求的UI组件)

文章图片
如果初始化组件需要进行大量处理或加载大量数据, 则可能会影响IDE的性能。在这种情况下, Android提供了一个漂亮的函数isInEditMode(), 该函数可用于限制在UI设计器中实际实例化该组件时使用的数据量。例如, 如果有很多事件要加载到CalendarView中, 我们可以在updateCalendar()函数内使用isInEditMode()在设计模式下提供一个空/受限事件列表, 否则加载真实事件。
6.调用组件
组件可以包含在XML布局文件中(用法示例可以在activity_main.xml中找到):
< samples.aalamir.customcalendar.CalendarView android:id="@+id/calendar_view" 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将以粗体蓝色显示当天, 并在其上放置事件标记:
Android自定义(如何构建可以满足你需求的UI组件)

文章图片
CalendarView显示事件
7.添加属性
Android提供的另一种功能是将属性分配给自定义组件。这使Android开发人员可以使用该组件通过布局XML选择设置, 并立即在UI设计器中查看结果, 而不必等待并查看CalendarView在运行时的样子。让我们增加更改组件中日期格式显示的功能, 例如, 拼写月份的全名, 而不是三个字母的缩写。
为此, 需要执行以下步骤:
  • 声明属性。我们将其称为dateFormat并为其指定字符串数据类型。将其添加到/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:id="@+id/calendar_view" 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);

]生成项目, 你会注意到UI设计器中显示的日期更改为使用月份的全名, 例如” 2015年7月” 。尝试提供不同的值, 看看会发生什么。
Android自定义(如何构建可以满足你需求的UI组件)

文章图片
更改CalendarView属性。
8.与组件交互
你是否尝试过按特定的日期?我们组件中的内部UI元素仍将以其正常的预期方式运行, 并将响应用户操作触发事件。那么, 我们如何处理这些事件呢?
答案包括两个部分:
  • 捕获组件内部的事件, 并
  • 向组件的父级报告事件(可以是片段, 活动或什至是另一个组件)。
第一部分非常简单。例如, 要处理长时间按下的网格项, 我们在组件类中分配一个相应的侦听器:
// 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()将父级提供的实现提供给日历视图。这是来自MainMainity.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自定义(如何构建可以满足你需求的UI组件)

文章图片
解决此问题的另一种更高级的方法是使用Android的Intent和BroadcastReceivers。当需要通知日历事件的多个组件时, 这特别有用。例如, 如果在日历中按一天, 则需要在” 活动” 中显示文本, 并由后台服务下载文件。
使用以前的方法将要求Activity向组件提供一个EventHandler, 处理该事件, 然后将其传递给Service。相反, 让组件广播一个Intent, 并且Activity和Service都通过它们自己的BroadcastReceivers接受它, 不仅使生活更轻松, 而且还有助于使所讨论的Activity和Service脱钩。
总结 看一下Android定制的强大功能!
鸣叫
因此, 这是你通过几个简单步骤创建自己的自定义组件的方式:
  • 创建XML布局并设置样式以适合你的需求。
  • 根据你的XML布局, 从适当的父组件派生你的组件类。
  • 添加组件的业务逻辑。
  • 使用属性可以使用户修改组件的行为。
  • 为了更轻松地在UI设计器中使用该组件, 请使用Android的isInEditMode()函数。
在本文中, 我们创建了一个日历视图作为示例, 主要是因为在许多方面缺少股票日历视图。但是, 你绝不限制可以创建哪种组件。你可以使用相同的技术来创建所需的任何东西, 天空是极限!
感谢你阅读本指南, 祝你在编码工作中一切顺利!

    推荐阅读