0%

虽然现在有了ExpandableListView可以代替ListView进行分组显示,但是不能在显示分组的同时显示无分组数据,做不到更高级的自定义,所以我在网上找了这篇文章,以后可以用得上。以下所有ListView均可替换为RecyclerView进行重新实现。

实现ListView分类显示效果,目前我知道的有两种方案:

  1. 每一个ItemView都包含用于显示分类信息的view(TitleView)和用于显示内容view(参考带索引的listview)
  2. 通过ListView加载不同类型的Item实现。本文属于这种实现方式

当前实现描述:

与自定义Adapter的ListVIew雷同,只是多使用两个BaseAdapter API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int getItemViewType (int position)  

获取通过getView为指定项目创建的视图的类型。
参数
position 在adapter数据里我们想知道视图类型的项目的位置
返回值
一个整形的视图类型的描述。如果一个视图通过getView(int, View, ViewGroup)方法转换成另一个视图,则两个视图将共享同一类型。注意:整形必须在0和getViewTypeCount()– 1之间。IGNORE_ITEM_VIEW_TYPE也可以返回。


public int getViewTypeCount ()

返回通过getView(int, View, ViewGroup))创建的视图的类型数量。每一个类型表示一组通过getView(int, View, ViewGroup)方法转换过的视图。如果adapter针对所有项目返回相同的视图类型,这个方法返回1。这个方法仅仅当adapter设置在AdapterView时调用。

返回值
通过这个adapter创建的视图类型的数量

视图 – 在Adapter.getView中通过getItemViewType获取item类型,当前实现为分类item与普通item两种,根据类型创建不同的VIew对象
数据 – 分类的数据通过List包含自定义数据对象Category,Category内部包含List用于存储当前Category item数据。
覆写BaseAdapter.areAllItemsEnabled 和 BaseAdapter.isEnabled两个方法,确保分类Item不可点击

代码:

Activity:
1
2
3
4
5
6
7
8
9
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class MainActivity extends Activity {  

private CategoryAdapter mCustomBaseAdapter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

ListView listView = (ListView) findViewById(R.id.listView1);

// 数据
ArrayList<Category> listData = getData();

mCustomBaseAdapter = new CategoryAdapter(getBaseContext(), listData);

// 适配器与ListView绑定
listView.setAdapter(mCustomBaseAdapter);

listView.setOnItemClickListener(new ItemClickListener());
}


private class ItemClickListener implements OnItemClickListener {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
Toast.makeText(getBaseContext(), (String)mCustomBaseAdapter.getItem(position),
Toast.LENGTH_SHORT).show();
}

}


/**
* 创建测试数据
*/
private ArrayList<Category> getData() {
ArrayList<Category> listData = new ArrayList<Category>();
Category categoryOne = new Category("路人甲");
categoryOne.addItem("马三立");
categoryOne.addItem("赵本山");
categoryOne.addItem("郭德纲");
categoryOne.addItem("周立波");
Category categoryTwo = new Category("事件乙");
categoryTwo.addItem("**贪污");
categoryTwo.addItem("**照门");
Category categoryThree = new Category("书籍丙");
categoryThree.addItem("10天学会***");
categoryThree.addItem("**大全");
categoryThree.addItem("**秘籍");
categoryThree.addItem("**宝典");
categoryThree.addItem("10天学会***");
categoryThree.addItem("10天学会***");
categoryThree.addItem("10天学会***");
categoryThree.addItem("10天学会***");
Category categoryFour = new Category("书籍丙");
categoryFour.addItem("河南");
categoryFour.addItem("天津");
categoryFour.addItem("北京");
categoryFour.addItem("上海");
categoryFour.addItem("广州");
categoryFour.addItem("湖北");
categoryFour.addItem("重庆");
categoryFour.addItem("山东");
categoryFour.addItem("陕西");

listData.add(categoryOne);
listData.add(categoryTwo);
listData.add(categoryThree);
listData.add(categoryFour);

return listData;
}
}
Adapter:
1
2
3
4
5
6
7
8
9
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public class CategoryAdapter extends BaseAdapter {  

private static final int TYPE_CATEGORY_ITEM = 0;
private static final int TYPE_ITEM = 1;

private ArrayList<Category> mListData;
private LayoutInflater mInflater;


public CategoryAdapter(Context context, ArrayList<Category> pData) {
mListData = pData;
mInflater = LayoutInflater.from(context);
}

@Override
public int getCount() {
int count = 0;

if (null != mListData) {
// 所有分类中item的总和是ListVIew Item的总个数
for (Category category : mListData) {
count += category.getItemCount();
}
}

return count;
}

@Override
public Object getItem(int position) {

// 异常情况处理
if (null == mListData || position < 0|| position > getCount()) {
return null;
}

// 同一分类内,第一个元素的索引值
int categroyFirstIndex = 0;

for (Category category : mListData) {
int size = category.getItemCount();
// 在当前分类中的索引值
int categoryIndex = position - categroyFirstIndex;
// item在当前分类内
if (categoryIndex < size) {
return category.getItem( categoryIndex );
}

// 索引移动到当前分类结尾,即下一个分类第一个元素索引
categroyFirstIndex += size;
}

return null;
}

@Override
public int getItemViewType(int position) {
// 异常情况处理
if (null == mListData || position < 0|| position > getCount()) {
return TYPE_ITEM;
}


int categroyFirstIndex = 0;

for (Category category : mListData) {
int size = category.getItemCount();
// 在当前分类中的索引值
int categoryIndex = position - categroyFirstIndex;
if (categoryIndex == 0) {
return TYPE_CATEGORY_ITEM;
}

categroyFirstIndex += size;
}

return TYPE_ITEM;
}

@Override
public int getViewTypeCount() {
return 2;
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

int itemViewType = getItemViewType(position);
switch (itemViewType) {
case TYPE_CATEGORY_ITEM:
if (null == convertView) {
convertView = mInflater.inflate(R.layout.listview_item_header, null);
}

TextView textView = (TextView) convertView.findViewById(R.id.header);
String itemValue = (String) getItem(position);
textView.setText( itemValue );
break;

case TYPE_ITEM:
ViewHolder viewHolder = null;
if (null == convertView) {

convertView = mInflater.inflate(R.layout.listview_item, null);

viewHolder = new ViewHolder();
viewHolder.content = (TextView) convertView.findViewById(R.id.content);
viewHolder.contentIcon = (ImageView) convertView.findViewById(R.id.content_icon);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}

// 绑定数据
viewHolder.content.setText( (String)getItem(position) );
viewHolder.contentIcon.setImageResource(R.drawable.ic_launcher);
break;
}

return convertView;
}


@Override
public boolean areAllItemsEnabled() {
return false;
}

@Override
public boolean isEnabled(int position) {
return getItemViewType(position) != TYPE_CATEGORY_ITEM;
}


private class ViewHolder {
TextView content;
ImageView contentIcon;
}

}
数据对象:
1
2
3
4
5
6
7
8
9
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 Category {  

private String mCategoryName;
private List<String> mCategoryItem = new ArrayList<String>();

public Category(String mCategroyName) {
mCategoryName = mCategroyName;
}

public String getmCategoryName() {
return mCategoryName;
}

public void addItem(String pItemName) {
mCategoryItem.add(pItemName);
}

/**
* 获取Item内容
*
* @param pPosition
* @return
*/
public String getItem(int pPosition) {
// Category排在第一位
if (pPosition == 0) {
return mCategoryName;
} else {
return mCategoryItem.get(pPosition - 1);
}
}

/**
* 当前类别Item总数。Category也需要占用一个Item
* @return
*/
public int getItemCount() {
return mCategoryItem.size() + 1;
}

}

1

需求

最近项目中需要用到日历控件,需求如下

  1. 同时支持周模式和月模式
  2. 支持边界设置,有些地方要求只显示当前周和下一周
  3. 支持日期禁用,某些特定的日期不允许点击
  4. 支持自定义的选中日期的样式
  5. 左右滑动自动切换周、月,同时自动选中同等位置的日期。

找来找去,最后发现GitHub的material-calendarview这个项目最贴近以上需求,稍作修改就能用了。

material-calendarview

项目开源地址:

https://github.com/prolificinteractive/material-calendarview

集成清单

  1. 添加compile 'com.prolificinteractive:material-calendarview:1.4.3'
  2. 添加日历控件到布局中
1
2
3
4
5
6
7
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:mcv_showOtherDates="all"
app:mcv_selectionColor="#00F"/>

功能展示

material-calendarview项目提供9个Sample来展示其功能用法

一、对照组:OldCalendarViewActivity

1
2
3
4
<CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="300dp"/>

采用系统控件CalendarView,实现一个点击显示日期的功能,可惜在低版本Android手机上没有material的样式,颜值很低,不能用。

二、基本用法:BasicActivity

  1. 绑定控件
1
2
@Bind(R.id.calendarView)
MaterialCalendarView widget;
  1. OnDateSelectedListener:用来监听选中日期的变化,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* The callback used to indicate a date has been selected or deselected
*/
public interface OnDateSelectedListener {

/**
* Called when a user clicks on a day.
* There is no logic to prevent multiple calls for the same date and state.
*
* @param widget the view associated with this listener
* @param date the date that was selected or unselected
* @param selected true if the day is now selected, false otherwise
*/
void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected);
}
  1. OnMonthChangedListener:用来接听页面滑动的变化,这个有点名不副实,因为在周模式的时候,滑动页面是按周来变化的。
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* The callback used to indicate the user changes the displayed month
*/
public interface OnMonthChangedListener {

/**
* Called upon change of the selected day
*
* @param widget the view associated with this listener
* @param date the month picked, as the first day of the month
*/
void onMonthChanged(MaterialCalendarView widget, CalendarDay date);
}
  1. getSelectedDate():获取被选中的日期。
1
2
3
4
5
6
7
private String getSelectedDatesString() {
CalendarDay date = widget.getSelectedDate();
if (date == null) {
return "No Selection";
}
return FORMATTER.format(date.getDate());
}

三、修饰选中日期:BasicActivityDecorated

  1. 设置日期范围
1
2
3
4
5
6
7
8
9
10
11
widget.setShowOtherDates(MaterialCalendarView.SHOW_ALL);
Calendar instance = Calendar.getInstance();
widget.setSelectedDate(instance.getTime());
Calendar instance1 = Calendar.getInstance();
instance1.set(instance1.get(Calendar.YEAR), Calendar.JANUARY, 1);
Calendar instance2 = Calendar.getInstance();
instance2.set(instance2.get(Calendar.YEAR), Calendar.DECEMBER, 31);
widget.state().edit()
.setMinimumDate(instance1.getTime())
.setMaximumDate(instance2.getTime())
.commit();
  1. 增加日期修饰
1
2
3
4
5
widget.addDecorators(
new MySelectorDecorator(this),
new HighlightWeekendsDecorator(),
oneDayDecorator
);

案例提供了三种修饰:

  1. 对选中日期增加指定背景图层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MySelectorDecorator implements DayViewDecorator {

private final Drawable drawable;

public MySelectorDecorator(Activity context) {
drawable = context.getResources().getDrawable(R.drawable.my_selector);
}

@Override
public boolean shouldDecorate(CalendarDay day) {
return true;
}

@Override
public void decorate(DayViewFacade view) {
view.setSelectionDrawable(drawable);
}
}
  1. 对周末增加指定背景图层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Highlight Saturdays and Sundays with a background
*/
public class HighlightWeekendsDecorator implements DayViewDecorator {

private final Calendar calendar = Calendar.getInstance();
private final Drawable highlightDrawable;
private static final int color = Color.parseColor("#228BC34A");

public HighlightWeekendsDecorator() {
highlightDrawable = new ColorDrawable(color);
}

@Override
public boolean shouldDecorate(CalendarDay day) {
day.copyTo(calendar);
int weekDay = calendar.get(Calendar.DAY_OF_WEEK);
return weekDay == Calendar.SATURDAY || weekDay == Calendar.SUNDAY;
}

@Override
public void decorate(DayViewFacade view) {
view.setBackgroundDrawable(highlightDrawable);
}
}
  1. 对选中日期增加指定修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Decorate a day by making the text big and bold
*/
public class OneDayDecorator implements DayViewDecorator {

private CalendarDay date;

public OneDayDecorator() {
date = CalendarDay.today();
}

@Override
public boolean shouldDecorate(CalendarDay day) {
return date != null && day.equals(date);
}

@Override
public void decorate(DayViewFacade view) {
view.addSpan(new StyleSpan(Typeface.BOLD));
view.addSpan(new RelativeSizeSpan(3.0f));
}

/**
* We're changing the internals, so make sure to call {@linkplain MaterialCalendarView#invalidateDecorators()}
*/
public void setDate(Date date) {
this.date = CalendarDay.from(date);
}
}
  1. 对特定日期增加红点修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Decorate several days with a dot
*/
public class EventDecorator implements DayViewDecorator {

private int color;
private HashSet<CalendarDay> dates;

public EventDecorator(int color, Collection<CalendarDay> dates) {
this.color = color;
this.dates = new HashSet<>(dates);
}

@Override
public boolean shouldDecorate(CalendarDay day) {
return dates.contains(day);
}

@Override
public void decorate(DayViewFacade view) {
view.addSpan(new DotSpan(5, color));
}
}

四、模式切换:SwappableBasicActivityDecorated

切换周模式、月模式

1
2
3
4
5
6
7
8
9
10
11
12
13
@OnClick(R.id.button_weeks)
public void onSetWeekMode() {
widget.state().edit()
.setCalendarDisplayMode(CalendarMode.WEEKS)
.commit();
}

@OnClick(R.id.button_months)
public void onSetMonthMode() {
widget.state().edit()
.setCalendarDisplayMode(CalendarMode.MONTHS)
.commit();
}

五、日期禁用:DisableDaysActivity

核心代码是:

1
2
DayViewFacade view
view.setDaysDisabled(true);

这个作者非常喜欢用装饰模式。所以对DayViewFacade的修饰可以无穷的累加。只要集成DayViewDecorator就可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Decorate Day views with drawables and text manipulation
*/
public interface DayViewDecorator {

/**
* Determine if a specific day should be decorated
*
* @param day {@linkplain CalendarDay} to possibly decorate
* @return true if this decorator should be applied to the provided day
*/
boolean shouldDecorate(CalendarDay day);

/**
* Set decoration options onto a facade to be applied to all relevant days
*
* @param view View to decorate
*/
void decorate(DayViewFacade view);

}

六、透过xml自定义控件:CustomizeXmlActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:mcv_showOtherDates="all"
app:mcv_arrowColor="?attr/colorPrimary"
app:mcv_leftArrowMask="@drawable/ic_navigation_arrow_back"
app:mcv_rightArrowMask="@drawable/ic_navigation_arrow_forward"
app:mcv_selectionColor="?attr/colorPrimary"
app:mcv_headerTextAppearance="?android:attr/textAppearanceMedium"
app:mcv_dateTextAppearance="@style/CustomDayTextAppearance"
app:mcv_weekDayTextAppearance="?android:attr/textAppearanceMedium"
app:mcv_weekDayLabels="@array/custom_weekdays"
app:mcv_monthLabels="@array/custom_months"
app:mcv_tileSize="36dp"
app:mcv_firstDayOfWeek="thursday"
app:mcv_calendarMode="week"/>

xml中能设置的属性很多,常用的如下:

  • mcv_showOtherDates:日期范围
  • mcv_firstDayOfWeek:一周的第一天
  • mcv_calendarMode:日历模式

七、透过java自定义控件:CustomizeCodeActivity

效果和xml是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
widget.setShowOtherDates(MaterialCalendarView.SHOW_ALL);
widget.setArrowColor(getResources().getColor(R.color.sample_primary));
widget.setLeftArrowMask(getResources().getDrawable(R.drawable.ic_navigation_arrow_back));
widget.setRightArrowMask(getResources().getDrawable(R.drawable.ic_navigation_arrow_forward));
widget.setSelectionColor(getResources().getColor(R.color.sample_primary));
widget.setHeaderTextAppearance(R.style.TextAppearance_AppCompat_Medium);
widget.setWeekDayTextAppearance(R.style.TextAppearance_AppCompat_Medium);
widget.setDateTextAppearance(R.style.CustomDayTextAppearance);
widget.setTitleFormatter(new MonthArrayTitleFormatter(getResources().getTextArray(R.array.custom_months)));
widget.setWeekDayFormatter(new ArrayWeekDayFormatter(getResources().getTextArray(R.array.custom_weekdays)));
widget.setTileSize((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 36, getResources().getDisplayMetrics()));

CalendarDay today = CalendarDay.from(2016, 5, 2);
widget.setCurrentDate(today);
widget.setSelectedDate(today);

widget.state().edit()
.setFirstDayOfWeek(Calendar.WEDNESDAY)
.setMinimumDate(CalendarDay.from(2016, 4, 3))
.setMaximumDate(CalendarDay.from(2016, 5, 12))
.setCalendarDisplayMode(CalendarMode.WEEKS)
.commit();

八、完整的设置展示DynamicSettersActivity

2

九、在Dialog中显示日历:DialogsActivity

3

拓展:滑屏时自动选中日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
widget.setOnDateChangedListener(new OnDateSelectedListener() {
@Override
public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) {
if (selected && date != null){
dayOfWeek = date.getCalendar().get(Calendar.DAY_OF_WEEK);
dayofMonth = date.getCalendar().get(Calendar.DAY_OF_MONTH);
}
Log.d(TAG, "date = " + date);
Log.d(TAG, "dayOfWeek = " + dayOfWeek);
Log.d(TAG, "dayofMonth = " + dayofMonth);
}
});

widget.setOnMonthChangedListener(new OnMonthChangedListener() {
@Override
public void onMonthChanged(final MaterialCalendarView widget, CalendarDay date) {
Calendar c = date.getCalendar();
Log.d(TAG, "date = " + date);
Calendar now = c;
if (widget.state().calendarMode == CalendarMode.WEEKS){
now.set(Calendar.DAY_OF_WEEK, c.get(Calendar.DAY_OF_WEEK) + dayOfWeek - 1);
}
else{
now.set(Calendar.DAY_OF_MONTH, c.get(Calendar.DAY_OF_MONTH) + dayofMonth - 1);
}
CalendarDay nowDate = CalendarDay.from(now);
Log.d(TAG, "nowDate = " + nowDate);
widget.setSelectedDate(nowDate);
}
});

Calendar instance = Calendar.getInstance();
widget.setSelectedDate(instance.getTime());

原文链接

在Xml文件中ListView可通过配置

1
2
android:dividerline
android:dividerHeight

如果divider需要缩进,可以在drawable新建一个list_divider.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle" >
<solid android:color="@color/list_background" />
</shape>
</item>
<item android:left="10dp"
android:right="10dp">
<shape android:shape="rectangle" >
<solid android:color="@color/divider_color"/>
</shape>
</item>
</layer-list>

然后在ListView中配置

1
2
3
4
5
<ListView
android:dividerHeight="@dimen/list_divider_height"
android:divider="@drawable/list_divider"
...
/>

另外list_divider.xml还有另一种写法:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="10dp"
android:insetRight="10dp"
android:drawable="@color/divider_color">
</inset>

ListView中的配置方法与之前相同

另外inset属性定义嵌入的可绘制资源。它必须是根元素。

属性(ATTRIBUTES):

xmlns:android: 字符串值,必须的。它定义了XML的命名空间,必须是:

android:drawable: 要绘制的资源,必须的,它指向一个要嵌入的可绘制资源。

android:insetTop: 尺寸值。用尺寸值或Dimension资源定义顶部的嵌入位置。

android:insetRight: 尺寸值。用尺寸值或Dimension资源定义右边的嵌入位置。

android:insetBottom: 尺寸值。用尺寸值或Dimension资源定义底部的嵌入位置。

android:insetLeft: 尺寸值。用尺寸值或Dimension资源定义左边的嵌入位置。

Google I/O 15上,谷歌发布了一个新的 support library,里面包含了一些遵循Material Design’s spec的UI组件,比如,AppbarLayout, CollapsingToolbarLayoutCoordinatorLayout。 这些组件配合起来使用可以产生强大的效果,那么让我们通过这篇文章来学习如何使用这些组件。

CoordinatorLayout

从名字可以看出,这个ViewGroup是用来协调它的子View的。看下图:

1.gif

这个例子中的各个View相互影响,却被和谐的组织在了一起。这就是使用CoordinatorLayout最简单的实例:

1
2
3
4
5
6
7
8
9
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?xml version="1.0" encoding="utf-8"?>

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:fitsSystemWindows="true">

<android.support.design.widget.AppBarLayout
android:id="@+id/main.appbar"
android:layout_width="match_parent"
android:layout_height="300dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true">

<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/main.collapsing"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp">

<ImageView
android:id="@+id/main.backdrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
android:src="@drawable/material_flat"
app:layout_collapseMode="parallax"/>

<android.support.v7.widget.Toolbar
android:id="@+id/main.toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:lineSpacingExtra="8dp"
android:text="@string/lorem"
android:padding="@dimen/activity_horizontal_margin"/>
</android.support.v4.widget.NestedScrollView>

<android.support.design.widget.FloatingActionButton
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_margin="@dimen/activity_horizontal_margin"
android:src="@drawable/ic_comment_24dp"
app:layout_anchor="@id/main.appbar"
app:layout_anchorGravity="bottom|right|end"/>
</android.support.design.widget.CoordinatorLayout>

看一下上面Layout的结构,CoordinatorLayout包含三个子View: 一个AppbarLayout,一个scrolleable View,一个指定了⚓锚点的FloatingActionBar

1
2
3
4
5
<CoordinatorLayout>
<AppbarLayout/>
<scrollableView/>
<FloatingActionButton/>
</CoordinatorLayout>

AppBarLayout

首先,AppBarLayout是一个LinearLayout,它的子View默认纵向排列, 可以通过一些参数控制子View的滑动行为。这么说你还是很难理解,所以无图无真相,上GIF:

2.gif

这张图最上面是一个可折叠图片(collapsing image),图片下面的蓝色View就是AppBarLayout,它包含了一个Toolbar,一个有标题和子标题的LinearLayout,一个带有Tab的TabLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
<AppBarLayout>
<CollapsingToolbarLayout
app:layout_scrollFlags="scroll|snap"/>

<Toolbar
app:layout_scrollFlags="scroll|snap"/>

<LinearLayout
android:id="+id/title_container"
app:layout_scrollFlags="scroll|enterAlways"/>

<TabLayout /> <!-- no flags -->
</AppBarLayout>

AppbarLayout的直接子View的操控行为,可以通过给子View添加layout_scrollFlags属性来控制。关于这个属性的值:scroll,在这个例子中用到了这个值。如果一个子View没有赋值scroll,那么滑动的时候,它就会一直静态显示,而其他scroll的View就会被划到它的后面隐藏。

另一个值snap的作用是避免一个View停留在动画的中间状态,也就是说滑动结束的时候一个View要么全部显示,要么全部隐藏,不会展示View的部分。

上面的LinearLayout指定了enterAlways,所以下拉的时候,它就会一直出现。TabLayout 没有指定,所以它一直静态显示。

由此,给子View使用不同的layout_scrollFlags就会生成不同的AppBarLayout。全部属性值参照官方文档,文章最后我也会提供几个放在Github上的实例。

AppbarLayout flags

*SCROLL_FLAG_ENTER_ALWAYS:((entering) / (scrolling on screen))下拉的时候,这个View也会跟着滑出。

*SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED:另一种enterAlways,但是只显示折叠后的高度。

*SCROLL_FLAG_EXIT_UNTIL_COLLAPSED:((exiting) / (scrolling off screen))上拉的时候,这个View会跟着滑动直到折叠。

*SCROLL_FLAG_SCROLL:跟着滑动方向滑动。

*SCROLL_FLAG_SNAP:滑动结束的时候,如果这个View部分显示,它就会滑动到离它最近的上边缘或下边缘。

CoordinatorLayout Behaviors

打开Android Studio (>= 1.4),用模版Scrolling Activity新建一个项目,然后直接编译运行。看到:

3.gif

查看生成的代码,没发现滑动时候Fab大小变化动画的相关代码,Why? 答案在FloatingActionButton的源代码中,感谢Android Studio v1.2自带了反编译源代码的功能,ctrl/cmd + click我们来FloatingActionButton的源码中究竟干了什么。

1
2
3
4
5
6
7
8
9
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
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Floating action buttons are used for a
* special type of promoted action.
* They are distinguished by a circled icon
* floating above the UI and have special motion behaviors
* related to morphing, launching, and the transferring anchor point.
*
* blah.. blah..
*/
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends ImageButton {
...

public static class Behavior
extends CoordinatorLayout.Behavior<FloatingActionButton> {

private boolean updateFabVisibility(
CoordinatorLayout parent, AppBarLayout appBarLayout,
FloatingActionButton child {

if (a long condition) {
// If the anchor's bottom is below the seam,
// we'll animate our FAB out
child.hide();
} else {
// Else, we'll animate our FAB back in
child.show();
}
}
}

...
}

其实那个大小变化动画是design包中的Behavior控制的,上面的CoordinatorLayout.Behavior<FloatingAcctionButton>控制显示或隐藏FAB,interesting?

SwipeDismissBehavior

在 design support library 中,我们发现了SwipeDismissBehavior,有了它,我们可以在CoordinatorLayout中轻松实现滑动删除功能。

4.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_swipe_behavior);
mCardView = (CardView) findViewById(R.id.swype_card);

final SwipeDismissBehavior<CardView> swipe = new SwipeDismissBehavior();

swipe.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_ANY);

swipe.setListener(
new SwipeDismissBehavior.OnDismissListener() {
@Override public void onDismiss(View view) {
Toast.makeText(SwipeBehaviorExampleActivity.this,
"Card swiped !!", Toast.LENGTH_SHORT).show();
}

@Override
public void onDragStateChanged(int state) {}
});

LayoutParams coordinatorParams = (LayoutParams) mCardView.getLayoutParams();

coordinatorParams.setBehavior(swipe);
}

Custom Behaviors

自定义Behavior并不难,首先介绍两个元素:child 和 dependency。

5.png

Childs and dependencies

要改变行为的那个View就是child,dependency是作为触发器影响child的那个View。这个例子中child是ImageView,dependency是Toolbar,然后,Toolbar滑动的时候,ImageView跟着滑动。

6.gif

下面开始创建自定义Behavior,首先,继承CoordinatorLayout.Behavior<T>T就是child的类型,上面例子是那个ImageView,然后我们重写方法 *layoutDependsOn *onDependentViewChanged 每当UI变化的时候就会调用layoutDependsOn,鉴定完dependency后一定要返回true。上面例子中用户一滑动就会自动调用layoutDependsOn,然后开始控制child 的行为。

1
2
3
4
5
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
CircleImageView, child, View dependency) {
return dependency instanceof Toolbar;
}

layoutDependsOn返回true后就开始调用onDependentViewChanged,在这个方法中我们利用dependency来实现动画,转换,动作。

1
2
3
4
5
6
7
8
9
10
public boolean onDependentViewChanged(CoordinatorLayout parent, 
CircleImageView avatar, View dependency) {
modifyAvatarDependingDependencyState(avatar, dependency);
}

private void modifyAvatarDependingDependencyState(
CircleImageView avatar, View dependency) {
// avatar.setY(dependency.getY());
// avatar.setBlahBlat(dependency.blah / blah);
}

放在一起就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class AvatarImageBehavior extends CoordinatorLayout.Behavior<CircleImageView> {

@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
CircleImageView, child, View dependency) {
return dependency instanceof Toolbar;
}

public boolean onDependentViewChanged(CoordinatorLayout parent,
CircleImageView avatar, View dependency) {
modifyAvatarDependingDependencyState(avatar, dependency);
}

private void modifyAvatarDependingDependencyState(
CircleImageView avatar, View dependency) {
// avatar.setY(dependency.getY());
// avatar.setBlahBlah(dependency.blah / blah);
}
}

原文

Resources

Coordinator Behavior Example- Github

Coordinator Examples- Github

Introduction to coordinator layout on Android- Grzesiek Gajewski

getDimension和getDimensionPixelOffset的功能类似,都是获取某个dimen的值,但是如果单位是dp或sp,则需要将其乘以density,如果是px,则不乘。并且getDimension返回float,getDimensionPixelOffset返回int。而getDimensionPixelSize则不管写的是dp还是sp还是px,都会乘以denstiy。

1
2
3
4
5
<resources>
<dimen name="dp_01">16dp</dimen>
<dimen name="px_01">16px</dimen>
<dimen name="sp_01">16sp</dimen>
</resources>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float a1=getResources().getDimension(R.dimen.dp_01);
int a2=getResources().getDimensionPixelOffset(R.dimen.dp_01);
int a3=getResources().getDimensionPixelSize(R.dimen.dp_01);
float b1=getResources().getDimension(R.dimen.px_01);
int b2=getResources().getDimensionPixelOffset(R.dimen.px_01);
int b3=getResources().getDimensionPixelSize(R.dimen.px_01);

float c1=getResources().getDimension(R.dimen.sp_01);
int c2=getResources().getDimensionPixelOffset(R.dimen.sp_01);
int c3=getResources().getDimensionPixelSize(R.dimen.sp_01);

Log.d("test", "getDimension= "+a1+", getDimensionPixelOffset="+a2+",getDimensionPixelSize="+a3);
Log.d("test", "getDimension= "+b1+", getDimensionPixelOffset="+b2+",getDimensionPixelSize="+b3);
Log.d("test", "getDimension= "+c1+", getDimensionPixelOffset="+c2+",getDimensionPixelSize="+c3);

LogCat中查看输出结果:

1
2
3
4
//Device (480*800,240dpi,density=1.5):
getDimension= 24.0, getDimensionPixelOffset=24,getDimensionPixelSize=24
getDimension= 16.0, getDimensionPixelOffset=16,getDimensionPixelSize=24
getDimension= 24.0, getDimensionPixelOffset=24,getDimensionPixelSize=24

从今天起,Evan 的小院正式开工啦,我会为小院添砖加瓦,欢迎大家常来小院逛一逛。一起学习,一起努力。hiahiahiahia~嗝