作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
艾哈迈德·阿拉米尔的头像

Ahmed Alamir

Ahmed是一名Android专家,对直观的用户体验和快速的应用程序性能充满热情.

工作经验

16

Share

对于开发人员来说,发现自己需要一个 UI 组件,它们所针对的平台要么不提供,要么提供, indeed, provided, 缺乏某种特性或行为的. 这两种情况的答案都是定制UI组件.

Android UI模型本质上是可定制的,提供了方法 安卓定制, testing以及创造的能力 自定义UI组件 in various ways:

  • 继承现有组件 (i.e. TextView, ImageView, etc.),并添加/覆盖所需的功能. For example, a CircleImageView that inherits ImageView, overriding the onDraw() 函数将显示的图像限制为一个圆圈,并添加一个 loadFromFile() 从外部存储器加载图像的函数.

  • 创建复合组件 从几个组成部分中. 这种方法通常利用 Layouts 控制组件在屏幕上的排列方式. For example, a LabeledEditText that inherits LinearLayout 具有水平方向,并且包含两个a TextView 作为一个标签和一个 EditText 作为文本输入字段.

    这种方法还可以利用前面的方法.e.,内部组件可以是本地的,也可以是定制的.

  • 最通用和最复杂的方法是 创建一个自绘制组件. 在这种情况下,组件将继承泛型 View 类和重写函数,如 onMeasure() 为了确定它的布局, onDraw() 显示其内容等. 以这种方式创建的组件通常严重依赖于Android的组件 2D drawing API.

Android定制案例分析 CalendarView

Android提供了一个 CalendarView component. 它性能良好,并提供了任何日历组件所期望的最低功能, 显示一个完整的月份并突出显示当前日期. 有些人可能会说它看起来也不错, 但只有当你想要一个本土的外观, 也没有兴趣定制它的外观.

例如, CalendarView 组件不提供更改如何标记特定日期或使用何种背景颜色的方法. 也没有办法添加任何自定义文本或图形,例如标记一个特殊的场合. 简而言之,组件是这样的,几乎没有什么可以改变:

Screenshot
CalendarView in AppCompact.Light theme.

Make Your Own

如何创建自己的日历视图? 以上任何一种方法都可行. However, 实用性通常会排除第三种选择(2D图像),而留给我们另外两种方法, 我们将在本文中混合使用这两种方法.

要继续学习,您可以找到源代码 here.

1. 组件布局

首先,让我们从组件的外观开始. 为了保持简单, 让我们在网格中显示天数, and, at the top, 月份的名称以及“下个月”和“上个月”按钮.

Screenshot
自定义日历视图.

该布局在文件中定义 control_calendar.xml, as follows. 注意,一些重复的标记被缩写为 ...:




   
   

      
      

      
      

      
      
   

   
   

      

      ... 重复MON - SAT.
   

   
   

2. 组件类

前面的布局可以原样包含在 Activity or a Fragment 它会很好地工作. 但是将其封装为独立的UI组件将防止代码重复并允许模块化设计, 每个模块处理一个职责.

我们的UI组件将是 LinearLayout,以匹配XML布局文件的根目录. 请注意,代码中只显示了重要的部分. 组件的实现驻留在 CalendarView.java:

公共类CalendarView扩展LinearLayout
{
   //内部组件
   private LinearLayout header;
   private ImageView btnPrev
   private ImageView btnNext;
   private TextView textdate;
   私有GridView网格;

   公共日历视图(Context Context)
   {
      super(context);
      initControl(上下文);
   }

   /**
    *加载组件XML布局
    */
   private void initControl(Context)
   {
      LayoutInflater = (LayoutInflater)
         context.getSystemService(上下文.LAYOUT_INFLATER_SERVICE);

      inflater.inflate(R.layout.control_calendar,);

      //布局是膨胀的,为组件分配局部变量
      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.id.calendar_date_display);
      grid = (GridView)findViewById.id.calendar_grid);
   }
}

代码非常简单. Upon creation, 组件将扩展XML布局, 当这一切完成后, 它将内部控制分配给局部变量,以便以后更容易访问.

3. 需要一些逻辑

要使该组件实际表现为日历视图,需要一些业务逻辑. 乍一看,这似乎很复杂,但实际上并没有太多的内容. 让我们来分析一下:

  1. 日历视图为7天宽, 并且可以保证所有月份都从第一行的某个地方开始.

  2. First, 我们需要算出这个月从什么位置开始, 然后用上个月的数字(30)填充之前的所有职位, 29, 28.. etc.),直到到达位置0.

  3. 然后,我们填写当月的天数(1、2、3……等等)。.

  4. 之后是下个月的天数(还是1,2,3).. 等),但这次我们只填充网格最后一行的剩余位置.

下面的图表说明了这些步骤:

Screenshot
自定义日历视图业务逻辑.

网格的宽度已指定为7个单元格, 表示周日历的, 但是高度呢? 网格的最大大小可以由最坏的情况确定,即从星期六开始的31天的一个月, 第一行的最后一个单元格是哪个, 并且还需要5行才能全部显示. So, 将日历设置为显示六行(总共42天)将足以处理所有情况.

但并非所有月份都有31天! 我们可以通过使用Android内置的日期功能来避免由此产生的复杂性, 避免了自己计算天数的需要.

的提供的日期功能 Calendar 类使实现非常简单. 在我们的组件中 updateCalendar() 函数实现这个逻辑:

updateCalendar()
{
   ArrayList cells = new ArrayList<>();
   日历日历=(日历)当前日期.clone();

   //确定当前月份开始的单元格
   calendar.set(Calendar.DAY_OF_MONTH, 1);
   int monthBeginningCell =日历.get(Calendar.Day_of_week) - 1;

   //将日历向后移动到一周的开始
   calendar.add(Calendar.DAY_OF_MONTH -monthBeginningCell);

   //填充单元格(根据我们的业务逻辑42天日历)
   while (cells.size() < DAYS_COUNT)
   {
      cells.add(calendar.getTime());
      calendar.add(Calendar.DAY_OF_MONTH, 1);
   }

   // update grid
   ((CalendarAdapter)网格.getAdapter()).updateData(细胞);

   // update title
   SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy");
   txtDate.setText(sdf.格式(currentDate.getTime()));
}

4. 可定制的心

因为负责显示单个日期的组件是 GridView,一个自定义日期显示方式的好地方是 Adapter,因为它负责保存单个网格单元格的数据和膨胀视图.

对于本例,我们将需要从我们的 CalendearView:

  • 今天应该在 bold blue text.
  • 当月以外的天数应该是 greyed out.
  • 有事件的日子应该显示一个特殊的图标.
  • 日历标题应该根据季节(夏季、秋季、冬季、春季)改变颜色。.

通过更改文本属性和背景资源,很容易实现前三个需求. 我们来实现a CalendarAdapter 来完成这个任务. 它非常简单,可以是类中的一个成员 CalendarView. 通过重写 getView() 功能上,我们可以实现以上要求:

@Override
public View getView(int position, View View, ViewGroup parent)
{
   //问题的日期
   Date = getItem(position);

   // today
   今天的日期= new Date();

   //如果条目不存在,则对其进行充气
   if (view == null)
      view = inflater.inflate(R.layout.Control_calendar_day, parent, false);

   //如果当天有事件,指定事件图像
   view.setBackgroundResource (eventDays.contains(date)) ?
      R.drawable.reminder : 0);

   // clear styling
   view.setTypeface (null,字体.NORMAL);
   view.setTextColor(颜色.BLACK);

   if (date.getMonth() != today.getMonth() ||
      date.getYear() != today.getYear())
   {
      //如果当天不在当前月份,则显示为灰色
      view.setTextColor (getresource ().getColor(R.color.greyed_out));
   }
   else if (date.getDate() ==今天.getDate())
   {
      //如果是今天,设置为蓝色/粗体
      view.setTypeface (null,字体.BOLD);
      view.setTextColor (getresource ().getColor(R.color.today));
   }

   // set text
   view.setText(String.valueOf(date.getDate()));

   return view;
}

最终的设计要求需要更多的工作. 首先,让我们加入四季的颜色 / res /价值/颜色.xml:

#44eebd82
#44d8d27e
#44a1c1da
#448da64b

Then, 让我们使用一个数组来定义每个月的季节(假设是北半球), for simplicity; sorry Australia!). In CalendarView 我们添加以下成员变量:

//四季的彩虹
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,1,2};

这样一来,选择合适的颜色就是通过选择合适的季节来完成的。monthSeason [currentMonth]),然后选取相应的颜色(彩虹(monthSeason [currentMonth]),这被添加到 updateCalendar() 以确保在更改日历时选择适当的颜色.

//根据当前季节设置标题颜色
int month =当前日期.get(Calendar.MONTH);
int season = monthSeason[month];
Int color = rainbow[season];

header.setBackgroundColor (getresource ().色鬼(颜色));

这样,我们得到以下结果:

安卓定制
球头颜色随季节变化.

Important Note due to the way HashSet 比较对象,上面的检查 eventDays.contains(date) in updateCalendar() 除非日期对象完全相同,否则不会为true. 它不执行任何特殊的检查 Date data type. 为了解决这个问题,这个检查被下面的代码取代:

for (Date eventDate: eventDays)
{
   if (eventDate.getDate() ==日期.getDate() &&
         eventDate.getMonth() ==日期.getMonth() &&
         eventDate.getYear() == date.getYear())
   {
      //标记这一天为事件
      view.setBackgroundResource(右.drawable.reminder);
      break;
   }
}

5. 它在设计时看起来很丑

Android在设计时对占位符的选择是有问题的. Fortunately, Android实际上会实例化我们的组件,以便在UI设计器中呈现它, 我们可以通过调用 updateCalendar() 在组件构造函数中. 这样,组件在设计时才真正有意义.

Screenshot

如果初始化组件会调用大量处理或加载大量数据, 它会影响IDE的性能. 在这种情况下,Android提供了一个漂亮的函数叫做 isInEditMode() 当组件在UI设计器中实际实例化时,可以用来限制使用的数据量. 例如,如果有很多事件要加载到 CalendarView, we can use isInEditMode() inside the updateCalendar() 函数在设计模式中提供空/有限事件列表,否则加载真实事件列表.

6. 调用组件

该组件可以包含在XML布局文件中(示例用法可在 activity_main.xml):


一旦布局被加载,检索并与之交互:

HashSet events = new HashSet<>();
events.add(new Date());

CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view));
cv.updateCalendar(事件);

上面的代码创建了 HashSet 的事件,向其添加当前日期,然后将其传递给 CalendarView. As a result, the CalendarView 将以蓝色粗体显示当前日期,并在其上放置事件标记:

Screenshot
CalendarView 显示事件

7. 添加属性

Android提供的另一个功能是为自定义组件分配属性. This allows Android开发者 使用组件通过布局XML选择设置,并立即在UI设计器中查看结果, 而不是等着看他们怎么 CalendarView 在运行时. 让我们添加在组件中更改日期格式显示的功能, 例如,拼写当月的全名,而不是三个字母的缩写.

要做到这一点,需要以下步骤:

  • 声明属性. Let’s call it dateFormat and give it string data type. Add it to /res/values/attrs.xml:

   
      
   

  • 在使用组件的布局中使用属性,并为其指定值 "MMMM yyyy":

  • 最后,让组件使用属性值:
typearray = getContext().obtainStyledAttributes (attrs R.styleable.CalendarView);

dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] 生成项目,您将注意到显示的日期更改 UI designer 使用月份的全名,比如“July 2015”. 尝试提供不同的值,看看会发生什么.

Screenshot
Changing the CalendarView attributes.

8. 与组件交互

你试过在特定的日子里按压吗? 组件中的内部UI元素仍然以正常的预期方式运行,并将触发事件以响应用户操作. 那么,我们如何处理这些事件?

答案包括两部分:

  • 捕获组件内部的事件,以及
  • 向组件的父组件报告事件(可以是 Fragment, an Activity 甚至是另一个组件).

第一部分非常简单. For example, 处理长按网格项, 我们在组件类中分配一个相应的监听器:

//长按一天
grid.setOnItemLongClickListener(新AdapterView.OnItemLongClickListener ()
{
   
   @Override
   public boolean onItemLongClick(AdapterView view, View cell, int position, long id)
   {
      //处理长按
      if (eventHandler == null)
      return false;

      Date date = view.getItemAtPosition(位置);
      eventHandler.onDayLongPress(日期);

      return true;
   }
});

有几种报告事件的方法. 一个直接而简单的方法是复制Android的方式:它为组件的事件提供一个接口,该接口由组件的父组件(eventHandler 在上面的代码片段中).

接口的函数可以传递与应用程序相关的任何数据. In our case, 接口需要公开一个事件处理程序, 哪个经过了按下的日期. 下面的接口定义在 CalendarView:

公共接口eventandler
{
   void onDayLongPress(日期);
}

父类提供的实现可以通过控件提供给日历视图 setEventHandler (). 下面是来自“MainActivity”的示例用法.java’:

@Override
onCreate(Bundle savedInstanceState)
{
   super.onCreate (savedInstanceState);
   setContentView(R.layout.activity_main);

   HashSet events = new HashSet<>();
   events.add(new Date());

   CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view));
   cv.updateCalendar(事件);
   
   //分配事件处理程序
   cv.setEventHandler(新CalendarView.EventHandler()
   {
      @Override
      公众无效onDayLongPress(日期)
      {
         //显示返回的日期
         DateFormat df = SimpleDateFormat.getDateInstance ();
         Toast.makeText (MainActivity.this, df.格式(日期),LENGTH_SHORT).show();
      }
   });
}

长按一天将触发一个长按事件,该事件被捕获并由 GridView 通过呼叫报告 onDayLongPress() 在提供的实现中,它反过来将在屏幕上显示所按日期:

Screenshot

另一种更高级的方法是使用Android Intents and broadcastreceiver. 当多个组件需要收到日历事件的通知时,这一点特别有用. 例如,如果按下日历中的某一天,则需要将文本显示在 Activity 还有一个文件要被后台下载 Service.

使用前面的方法需要 Activity to provide an EventHandler 到组件,处理事件,然后将其传递给 Service. 相反,让组件广播一个 Intent and both the Activity and Service 通过自己的方式接受 broadcastreceiver 不仅使生活更轻松,而且还有助于解耦 Activity and the Service in question.

Conclusion

看看Android定制的强大力量吧!

所以,这就是你如何在几个简单的步骤中创建自己的自定义组件:

  • 创建XML布局并对其进行样式设置以满足您的需要.
  • 根据XML布局,从适当的父组件派生组件类.
  • 添加组件的业务逻辑.
  • 使用属性使用户能够修改组件的行为.
  • 为了在UI设计器中更容易地使用组件,请使用Android的 isInEditMode() function.

In this article, 我们创建了一个日历视图作为示例, 主要是因为股票日历视图是, in many ways, lacking. 但是,对于可以创建的组件类型,您没有任何限制. 你可以用同样的技术创造任何你需要的东西,天空是极限!

感谢您阅读本指南,祝您在编码工作中好运!

聘请Toptal这方面的专家.
Hire Now
艾哈迈德·阿拉米尔的头像
Ahmed Alamir

Located in 墨尔本,维多利亚,澳大利亚

Member since June 11, 2014

About the author

Ahmed是一名Android专家,对直观的用户体验和快速的应用程序性能充满热情.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

16

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal开发者

Join the Toptal® community.