本文概述
- Android自定义案例研究:CalendarView
- 自己做
- 总结
【Android自定义(如何构建可以满足你需求的UI组件)】Android UI模型具有固有的可自定义性, 它提供了Android自定义, 测试和通过各种方式创建自定义UI组件的功能:
- 继承现有组件(即TextView, ImageView等), 并添加/覆盖所需的功能。例如, 一个CircleImageView继承ImageView, 重写onDraw()函数以将显示的图像限制为一个圆形, 并添加loadFromFile()函数以从外部存储器加载图像。
- 从多个组件中创建一个复合组件。这种方法通常利用布局来控制组件在屏幕上的排列方式。例如, 一个LabeledEditText继承了水平方向的LinearLayout, 并且同时包含充当标签的TextView和充当文本输入字段的EditText。
这种方法也可以利用前一种方法, 即内部组件可以是本地的或自定义的。
- 最通用, 最复杂的方法是创建一个自绘组件。在这种情况下, 组件将继承通用的View类, 并覆盖诸如onMeasure()以确定其布局, onDraw()来显示其内容等功能。以这种方式创建的组件通常在很大程度上取决于Android的2D绘图API。
例如, CalendarView组件无法更改某天的标记方式或使用的背景颜色。例如, 也无法添加任何自定义文本或图形来标记特殊情况。简而言之, 组件看起来像这样, 几乎什么都不能更改:
文章图片
AppCompact.Light主题中的CalendarView。
自己做 那么, 如何创建自己的日历视图呢?上面的任何方法都可以。但是, 实用性通常会排除第三个选项(2D图形), 而让我们剩下其他两种方法, 因此在本文中我们将两者结合使用。
接下来, 你可以在此处找到源代码。
1.组件布局
首先, 让我们从组件的外观开始。为简单起见, 让我们在网格中显示日期, 并在顶部显示月份名称以及” 下个月” 和” 上个月” 按钮。
文章图片
自定义日历视图。
此布局在文件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.需要一些逻辑
为了使该组件实际充当日历视图, 需要执行一些业务逻辑。乍一看似乎很复杂, 但实际上并没有太多。让我们分解一下:
- 日历视图的宽度为7天, 可以确保所有月份都从第一行的某个位置开始。
- 首先, 我们需要弄清该月的开始位置, 然后使用前一个月的数字(30、29、28等)填充之前的所有位置, 直到到达位置0。
- 然后, 我们填写当月的日期(1、2、3…等)。
- 在那之后是下个月的日子(再次是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中要求以下内容:
- 当天应以蓝色粗体显示。
- 当前月份以外的天数应设为灰色。
- 发生事件的日子应显示一个特殊的图标。
- 日历标题应根据季节(夏季, 秋季, 冬季, 春季)更改颜色。
@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));
这样, 我们得到以下结果:
文章图片
标题颜色根据季节而变化。
重要说明:由于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()来利用它。这样, 组件将在设计时真正有意义。
文章图片
如果初始化组件需要进行大量处理或加载大量数据, 则可能会影响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将以粗体蓝色显示当天, 并在其上放置事件标记:
文章图片
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月” 。尝试提供不同的值, 看看会发生什么。
文章图片
更改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的Intent和BroadcastReceivers。当需要通知日历事件的多个组件时, 这特别有用。例如, 如果在日历中按一天, 则需要在” 活动” 中显示文本, 并由后台服务下载文件。
使用以前的方法将要求Activity向组件提供一个EventHandler, 处理该事件, 然后将其传递给Service。相反, 让组件广播一个Intent, 并且Activity和Service都通过它们自己的BroadcastReceivers接受它, 不仅使生活更轻松, 而且还有助于使所讨论的Activity和Service脱钩。
总结 看一下Android定制的强大功能!
鸣叫
因此, 这是你通过几个简单步骤创建自己的自定义组件的方式:
- 创建XML布局并设置样式以适合你的需求。
- 根据你的XML布局, 从适当的父组件派生你的组件类。
- 添加组件的业务逻辑。
- 使用属性可以使用户修改组件的行为。
- 为了更轻松地在UI设计器中使用该组件, 请使用Android的isInEditMode()函数。
感谢你阅读本指南, 祝你在编码工作中一切顺利!
推荐阅读
- OpenCV教程(在iOS中使用MSER进行实时对象检测)
- 强制触摸对UI和UX意味着什么()
- 正在为Android Auto和Apple Carplay之类的汽车信息娱乐系统开发下一件大事吗()
- 通过Mantle和Realm简化iOS上的RESTful API使用和数据持久化
- 适用于开发人员的Apple Pay和Android Pay
- 缓慢采用Android Wear令人窒息
- 适用于开发人员的iOS 9 Beta和WatchOS 2
- 从应用程序启动android浏览器
- 在中查找最近标记的邮件邮件.app使用appscript