夏霂熠雨


  • 首页

  • 分类

  • 归档

  • 关于

RecycleView 系列(4)--利用 ItemDecoration 实现粘性头部分组功能

发表于 2019-03-07 | 分类于 android | 阅读次数: ℃

前言

上一篇文章介绍了用 ItemDecoration 实现物流时间轴样式的一个例子,接下来,再来看一个比较常见的功能:粘性头部分组功能。

我们先来看一下今日头条中选取位置信息时的效果:
1.gif
可以看到这个效果包含下面几个点:

  • 数据分组展示
  • 分组头部悬停
  • 两个分组头部相遇时上面的分组头部被缓缓推出,同时透明度发生变化

下面就来借用 ItemDecoration 来一点点实现它。

在了解 ItemDecoration 之前,这个功能的实现可能会采取在 item 中包含用于展示城市数据的控件和展示头部分组的控件,然后只有每组的第一个数据展示分组控件,其余默认隐藏掉。那现在要用 ItemDecoration 来实现,其实也很简单,每个头部分组就是一种特殊的分割线,组内其他元素采用另一种分割线。

有了思路,然后让我们先开始造一点用于展示的数据,这里我把一些市区信息存在 json 文档中。为了便于我们在 ItemDecoration 类只进行分割线的绘制,所以我这里对数据提前进行处理,确保在绘制分割线之前,已经知道哪些数据是分组的头部,哪些数据是组内其他元素。

分组逻辑为:第一个数据肯定是分组的头部,然后从第二个数据开始,拿当前数据和上个数据进行比较,如果首字母相等,说明当前数据仍然是这个分组内的数据,如果不一样,说明产生了新的分组,那么当前这个数据就是这个分组的头部。

下面是部分进行数据处理的代码(默认数据已经按首字母分组):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<CityBean> cityBeans = JSONArray.parseArray(cityJson, CityBean.class);
if (cityBeans ==null && cityBeans.size() ==0){
return;
}
for (int i = 0; i < cityBeans.size() ; i++) {
CityBean cityBean = cityBeans.get(i);
//获取当前数据的首字母
String firstLetter = cityBean.getPinyin() ==null? "#" :cityBean.getPinyin().substring(0,1);
cityBean.setFirstLetter(firstLetter);
//第一个数据肯定是分组头部
if (i ==0){
cityBean.setGroupFirst(true);
}else {
//其余数据要跟上个数据进行比较
CityBean lastCityBean = cityBeans.get(i-1);
String lastLetter = lastCityBean.getPinyin() ==null? "#" :lastCityBean.getPinyin().substring(0,1);
//当前数据首字母和上个数据首字母相等,说明还是组内数据,不相等,出现新的分组,当前数据是分组头部
cityBean.setGroupFirst(!firstLetter.equals(lastLetter));
}
cityBeanList.add(cityBean);
}

接下来我们来绘制分割线。首先需要设置 getItemOffsets()方法,为 item 预留出分割线的位置(分组头部分割线高 20dp,其余组内元素分割线 1px):

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
/**
* 设置 item top方向的偏移量
*/
private int topOffset ;

public GroupDivider(Context context, List<CityBean> cityBeanList) {
this.context = context;
this.cityBeanList = cityBeanList;
paint= new Paint();
paint.setAntiAlias(true);
paint.setColor(context.getResources().getColor(R.color.black));
paint.setTextSize(18);
paint.setTextAlign(Paint.Align.CENTER);
fontMetrics = paint.getFontMetrics();
topOffset = ScreenUtil.dip2px(context,20);
}

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);

final int position = parent.getChildAdapterPosition(view);
final CityBean cityBean = cityBeanList.get(position);

if ( cityBean .isGroupFirst()){
//头部分组元素
outRect.top = topOffset;
}else {
//组内元素
outRect.top = 0;
}
//每个组内元素的分割线高度
outRect.bottom = 1;

}

然后在 onDraw() 或 onDrawOver() 方法中进行分割线的绘制。我们知道,这两个方法的区别是 onDraw() 的绘制顺序早于 item 的绘制,所以会被 item 覆盖,而我们接下来是要实现悬停的功能,也就是头部分组的分割线要始终在 最上层,不能被 item 覆盖。所以,我们这里采取在 onDrawOver() 方法中进行分割线的绘制。

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
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
canvas.save();
//获取文字基准等信息
float topTxt = fontMetrics.top;
float bottomTxt = fontMetrics.bottom;
int baseLineY = (int) (topTxt/2 + bottomTxt/2);
final int childCount = parent.getChildCount();
//遍历 ReycleView 当前可见 item
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int position = parent.getChildAdapterPosition(child);
CityBean cityBean = cityBeanList.get(position);
//获取分组字母
final String text = cityBean.getPinyin().substring(0,1).toUpperCase();
//如果是分组的第一条数据,绘制分组头部
if (cityBean.isGroupFirst()){
paint.setColor(context.getResources().getColor(R.color.gray));
canvas.drawRect(0,child.getTop()-topOffset,parent.getRight(),child.getTop(),paint);
paint.setColor(context.getResources().getColor(R.color.black));
canvas.drawText(text,30,child.getTop()-topOffset/2-baseLineY,paint);
}
}

canvas.restore();
}

运行,看下效果:
12.gif
可以看到,分组功能已经实现。下面接着看第二要点:头部悬停

2.设置头部悬停

实现头部悬停的原理也很简单:不管当前元素是不是分组内的第一个元素,只要是当前 RecycleView 可见的第一个 item ,就需要绘制分组头部。

因为我们在 onDrawOver() 方法中根据 parent.getChildCount() 获取到的就是可见的 item ,所以下面稍微改动下 onDrawOver() 方法,添加第一条可见 item 逻辑判断:

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
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
canvas.save();
//获取文字基准等信息
float topTxt = fontMetrics.top;
float bottomTxt = fontMetrics.bottom;
int baseLineY = (int) (topTxt/2 + bottomTxt/2);
final int childCount = parent.getChildCount();
//遍历 ReycleView 当前可见 item
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int position = parent.getChildAdapterPosition(child);
CityBean cityBean = cityBeanList.get(position);
//获取分组字母
final String text = cityBean.getPinyin().substring(0,1).toUpperCase();
//可见的第一条数据不管是不是分组的第一个,都要绘制分组 header
if (i == 0){
paint.setColor(context.getResources().getColor(R.color.gray));
//获取 recycleview 的 paddingTop 的值,防止悬停头部分组位置计算不对
int top = parent.getPaddingTop();
//绘制40px的矩形框
canvas.drawRect(0,top,parent.getRight(),topOffset,paint);
paint.setColor(context.getResources().getColor(R.color.black));
//绘制分组字母文字
canvas.drawText(text,30,top+topOffset/2-baseLineY,paint);
}else {
//可见的非第一条数据,如果是分组的第一条数据,绘制分组头部
if (cityBean.isGroupFirst()){
paint.setColor(context.getResources().getColor(R.color.gray));
canvas.drawRect(0,child.getTop()-topOffset,parent.getRight(),child.getTop(),paint);
paint.setColor(context.getResources().getColor(R.color.black));
canvas.drawText(text,30,child.getTop()-topOffset/2-baseLineY,paint);
}
}
}
canvas.restore();
}

然后运行,看下效果:
3.gif
可以看到,头部悬停功能已经很好的实现了。但是跟头条比,在两个头部相遇切换的时候比较生硬,可以仔细看一下动图的最后面一部分,下一个分组头部是直接覆盖掉上一个分组头部的。

所以,接下来,一起来看下第三部分推动效果的实现。

3.设置推出动画效果

首先,再来仔细的看一下这个推出的过程:
4.gif
然后这里截取一下整个过程中的几个片段:
5.jpg
仔细分析一下这个图,然后思考下下面几个问题:

Q:什么时候两个头部分组快要相遇了?

A:由图一到图二,分析可知,是 A3 这个 item 慢慢的靠近区分 A,A2 这个 item 慢慢滑出屏幕,A3 成为 RecycleView 的第一个可见的 item 的时候。
而 A3 有没有什么特殊的地方?有的,A3 是 A 这个分组组内的最后一个元素。

Q:两个分组头部相遇的临界点满足什么条件?

A:由图三分析,临界条件为 A3 成为 RecycleView 第一个可见元素,并且 A3 这个 item 的 bottom 坐标值是分区 A 的 bottom的坐标值, A3 的 bottom 坐标值 - A 的高度 = RecycleView 的 可见上边界值(RecycleView 起点,去掉 RecycleView 的 paddingTop)

Q:两个分组头部相遇后,A 分区的坐标值怎么变化?

因为我们是需要在onDrawOver() 方法中绘制 A 区域,所以需要计算 A 的 top 坐标值 和 bottom 坐标值,由图四可以得出,A 的 top 值 = A3的 bottom - A 的高度,并且这个值肯定是小于 0(或者 RecycleView 的 paddingTop 值)

来看一下具体代码实现:

首先我们需要判断一条数据是不是组内的最后一条数据,所以改造下最开始的数据处理部分的代码,加上设置是否是组内最后一条数据的标志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (int i = 0; i < cityBeans.size() ; i++) {
CityBean cityBean = cityBeans.get(i);
//获取当前数据的首字母
String firstLetter = cityBean.getPinyin() ==null? "#" :cityBean.getPinyin().substring(0,1);
cityBean.setFirstLetter(firstLetter);
//第一个数据肯定是分组头部
if (i ==0){
cityBean.setGroupFirst(true);
}else {
//其余数据要跟上个数据进行比较
CityBean lastCityBean = cityBeans.get(i-1);
String lastLetter = lastCityBean.getPinyin() ==null? "#" :lastCityBean.getPinyin().substring(0,1);
//当前数据首字母和上个数据首字母相等,说明还是组内数据,不相等,出现新的分组,当前数据是分组头部
cityBean.setGroupFirst( ! firstLetter.equals(lastLetter));
//当前数据首字母和上个数据首字母相等,说明上一条数据不是组内最后条数据,不相等,出现新的分组,上条数据就是其组内最后一条元素
lastCityBean.setGroupLast( ! firstLetter.equals(lastLetter));
}
cityBeanList.add(cityBean);
}

然后修改一下 onDrawOver() 方法中 第一条可见数据时的逻辑:

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
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
canvas.save();
//获取文字基准等信息
float topTxt = fontMetrics.top;
float bottomTxt = fontMetrics.bottom;
int baseLineY = (int) (topTxt/2 + bottomTxt/2);
final int childCount = parent.getChildCount();
//遍历 ReycleView 当前可见 item
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int position = parent.getChildAdapterPosition(child);
CityBean cityBean = cityBeanList.get(position);
//获取分组字母
final String text = cityBean.getPinyin().substring(0,1).toUpperCase();
//可见的第一条数据不管是不是分组的第一个,都要绘制分组 header
if (i == 0){
paint.setColor(context.getResources().getColor(R.color.gray));
//获取 recycleview 的 paddingTop 的值,防止悬停头部分组位置计算不对
int top = parent.getPaddingTop();
//是分组内最后一个元素
if (cityBean.isGroupLast()){
//计算头部推出后的top的坐标值
int diffTop = child.getBottom() - topOffset;
//需要推出去了
if (diffTop < top){
top = diffTop;
//设置推出去后的分组头部颜色浅一点
paint.setColor(context.getResources().getColor(R.color.gray_light));
}
}
//绘制40px的矩形框
canvas.drawRect(0,top,parent.getRight(),topOffset,paint);
paint.setColor(context.getResources().getColor(R.color.black));
//绘制分组字母文字
canvas.drawText(text,30,top+topOffset/2-baseLineY,paint);
}else {
//可见的非第一条数据,如果是分组的第一条数据,绘制分组头部
if (cityBean.isGroupFirst()){
paint.setColor(context.getResources().getColor(R.color.gray));
canvas.drawRect(0,child.getTop()-topOffset,parent.getRight(),child.getTop(),paint);
paint.setColor(context.getResources().getColor(R.color.black));
canvas.drawText(text,30,child.getTop()-topOffset/2-baseLineY,paint);
}
}
}
canvas.restore();
}

至此,整个逻辑完成,运行,看一下效果:
6.gif

总结

关于 ItemDecoration 类的使用就到这里了,结合这几篇文章,我们可以发现,只要是用到 ItemDecoration 类实现的,步骤都很固定,第一步利用 getItemOffsets()方法,为 item 预留出分割线的位置,第二部在 onDraw()或 onDrawOver() 方法中进行绘制。

其实比较难的就是分析实现的过程,和各个点坐标的计算。

参考

https://blog.csdn.net/briblue/article/details/70211942
https://juejin.im/post/5a4551ce51882512d82305cb
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration

RecycleView 系列(3)-- 利用 ItemDecoration 实现时光轴(物流时间)样式类

发表于 2019-02-20 | 分类于 android | 阅读次数: ℃

前言

上一篇文章中详细介绍了 ItemDecoration 这个了类,了解了 RecycleView 实现分割线的原理。

下面我们来进入实战篇,首先实现一个比较常见的时光轴(物流详情)的效果。

一、 效果分析

首先我来分析一下常见的一种物流详情效果图:
1.jpg
分析可知,常规的 item 布局实现无法满足此需求,因为我们在 layout 中不知道 item 的高度为多少,中间那条竖线不容易实现。
有了上节对 ItemDecoration 的了解,我们可以将左边布局(蓝色框以左) 当做分割线来处理。

其中需要注意的有:

  1. 红色框内表示整个 item 内容
  2. 蓝色框内 item 在 布局中 固定的内容
  3. 绿色框内小图标的绘制
  4. 黄色框内时间的绘制
  5. 灰色框内竖线的绘制

二、功能实现

既然知道了思路,根据上节的步骤按部就班的就好。

1.首先模拟下数据结构,定义好 dataBean,如下:

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
public class LogisticsInfoBean  implements Serializable {
/**
* 物流信息
*/
private String message;
/**
* 当前物流状态
*/
private LogisticsStatus status;
/**
* 日期
*/
private String date;
/**
* 时间
*/
private String time;
}

public enum LogisticsStatus {
/**
* 一般提示语
*/
TIPS,
/**
* 已下单
*/
ORDERED,
/**
* 备货中
*/
STOCK_UP ,
/**
* 已发货
*/
DELIVERED,
/**
* 运输中
*/
TRANSPORTING,
/**
* 已收货
*/
RECEIVING
}

2.设置 getItemOffsets()

因为我们作为分割线的部分是蓝色框以左,所以我们需要设置 item 左边 left 的边距,假设预留 120 像素的边距。

1
2
3
4
5
6
7
8
9
10
11
/**
* 设置 item lef他方向的偏移量
*/
private int leftOffset = 120;

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//设置item 左边流出 leftOffset 的边距
outRect.left = leftOffset;
}
  1. 通过 onDraw() 方法绘制左边分割线内的内容

绘制是主要的部分,而 onDraw() 方法 作用于 RecycleView, 所以我们需要遍历 item ,计算绘制内容的坐标,
然后通过 canvas 的 draw 方法进行绘制。

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
/**
* 画的小圆点的半径
*/
private int circleRadius = 10;
/**
* 画的小图标的宽度
*/
private int iconWidth = 50;
private Context context;
private int padding = 24;
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
Log.d(TAG,"----onDraw---");
canvas.save();
//先画分割线整体背景色
canvas.drawColor(context.getResources().getColor(R.color.white));
final int childCount = parent.getChildCount();
//遍历
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
//1.先画 1 px 的图标上半部分的竖线
int startX = leftOffset-30;
int startY = child.getTop();
int lineStopY = startY+padding;
paint.setColor(context.getResources().getColor(R.color.gray_deep));
canvas.drawLine(startX,startY,startX,lineStopY,paint);
//2.画图形
int positon = parent.getChildAdapterPosition(child);
LogisticsInfoBean bean = dataBeanList.get(positon);
Rect dst = new Rect(startX-iconWidth/2,lineStopY,startX+iconWidth/2,lineStopY+iconWidth);
//根据不同状态画不同图形
switch (bean.getStatus()){
case TIPS:
canvas.drawCircle(startX,lineStopY+circleRadius,circleRadius,paint);
break;
case ORDERED:
canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_order),null,dst,null);
break;
case STOCK_UP:
canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_stockup),null,dst,null);
break;
case DELIVERED:
canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_diliver),null,dst,null);
break;
case TRANSPORTING:
canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_transporting),null,dst,null);
break;
case RECEIVING:
canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_receive),null,dst,null);
break;
default:
canvas.drawCircle(startX,lineStopY+circleRadius,circleRadius,paint);
break;
}
//画下半部分竖线
if (positon != dataBeanList.size() -1){
if (bean.getStatus() == LogisticsStatus.TIPS){
canvas.drawLine(startX,lineStopY+2*circleRadius,startX,child.getBottom(),paint);
}else {
canvas.drawLine(startX,lineStopY+iconWidth,startX,child.getBottom(),paint);
}
}
//3.画日期
canvas.drawText(bean.getDate(),startX-iconWidth/2-10,lineStopY+iconWidth/2,paint);
canvas.drawText(bean.getTime(),startX-iconWidth/2-10,lineStopY+iconWidth/2+20,paint);

}


canvas.restore();
}

4.设置数据源

让我们来造点假数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void initData() {
dataBeanList.add(new LogisticsInfoBean("[收货地址,xxxxxxx]",LogisticsStatus.RECEIVING,"02-11","10:00"));
dataBeanList.add(new LogisticsInfoBean("小主,运输中x1",LogisticsStatus.TRANSPORTING,"02-10","12:00"));
dataBeanList.add(new LogisticsInfoBean("小主,\n运输中x2",LogisticsStatus.TRANSPORTING,"02-10","12:10"));
dataBeanList.add(new LogisticsInfoBean("小主,\n\n运输中x3",LogisticsStatus.TRANSPORTING,"02-10","12:20"));
dataBeanList.add(new LogisticsInfoBean("小主,\n运输中x4",LogisticsStatus.TRANSPORTING,"02-10","12:30"));
dataBeanList.add(new LogisticsInfoBean("小主,已发货",LogisticsStatus.DELIVERED,"02-10","10:00"));
dataBeanList.add(new LogisticsInfoBean("小主,备货中",LogisticsStatus.STOCK_UP,"02-09","12:00"));
dataBeanList.add(new LogisticsInfoBean("订单支付成功,系统正在处理",LogisticsStatus.ORDERED,"02-09","10:10"));
dataBeanList.add(new LogisticsInfoBean("订单创建成功,等待支付",LogisticsStatus.TIPS,"02-09","10:00"));

adapter.notifyDataSetChanged();

}

其他一些设置这里就不在说了,然后运行看一下效果:
2.jpg
所以,实现起来也很简单有没有。

完整代码看这里:demo 传送门

总结

关于分割线的实现,不管是什么样式的,按照之前说的步骤一步步来就好:

  • 通过 getItemOffset() 方法设置 item 的偏移量**
  • 在 onDraw()或 onDrawOver() 方法中完成绘制**
  • 遍历 item,计算分割线的位置**
  • 通过 draw()方法完成绘制**

其中需要注意的是各个绘制内容坐标的计算。

参考

Android 自定义View实战系列 :时间轴
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration

RecycleView 系列(2)-- 认识 ItemDecoration 类

发表于 2019-02-10 | 分类于 android | 阅读次数: ℃

前言

上一篇 博客介绍了 RecycleView 的基本使用,接下来我们来给列表添加点装饰 - 分割线。

RecycleView 没有像 ListView 一样可以直接在 xml 中或者通过 setDivider()方法设置分割线的方法。它是通过 RecycleView 的 addItemDecoration(ItemDecoration decor) 方法来设置的。很显然,我们需要传入一个 ItemDecoration 对象,这个对象是一个抽象类,官方已经提供了一种常用分割线类:DividerItemDecoration。来看一下用法:

1
2
3
// 这里 LayoutManager 为 LinearLayoutManager
DividerItemDecoration itemDecoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
recycleView.addItemDecoration(itemDecoration )

也很简单有木有,运行代码看下效果:
1.png
那这个简单的效果是如何实现的呢?前面说过 RecycleView 的分割线是由 RecyclerView.ItemDecoration 这个类来实现的,所以我们要先来了解一下这个类。

一.ItemDecoration 类介绍

惯例先看一下文档中对 ItemDecoration 类的介绍:

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter’s data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.
All ItemDecorations are drawn in the order they were added, before the item views (in onDraw() and after the items (in onDrawOver(Canvas, RecyclerView, RecyclerView.State).
ItemDecoration 允许应用程序为特定的 item 视图添加一个特殊的绘图和布局偏移量。这对于绘制 items 之间的分隔线、高亮显示、可视分组等非常有用。
所有的 item 的装饰都是按照被添加的顺序绘制的,先于 item 的绘制(在onDraw() 方法中),和在 item 绘制后 (onDrawOver(Canvas, RecyclerView, RecyclerView.State)方法中)。

从这段介绍中,我们可以得到以下信息:

  • ItemDecoration 可以为 item 添加绘图,还可以设置偏移量
  • ItemDecoration 可以用于实现 item 之间的分隔线、高亮显示、可视分组等功能
  • ItemDecoration 中的 onDraw() 方法先于 item 绘制,onDrawOver(Canvas, RecyclerView, RecyclerView.State方法执行顺序在 item 的绘制之后

让我们来新建一个类,继承 RecyclerView.ItemDecoration,默认会要实现它的三个抽象方法:

1
2
3
4
5
6
7
8
9
10
public class BasicDivider extends RecyclerView.ItemDecoration {
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
}
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
}

看一下文档中对这三个方法的说明:

1.getItemOffsets () 方法:

  • 重新得到给定 item 的任何偏移量。outRect 的每个字段指定 item 视图应该插入多少像素,效果类似于 padding 或 margin。默认将 outRect 的边界设置为 0 并返回;
  • .如果这个 ItemDecoration 不影响 item 视图的位置,在 return 之前要先将 outRect的所有四个字段 (left, top, right, bottom) 设为 0;
  • 如果您需要访问适配器以获取额外的数据,您可以调用 getChildAdapterPosition(View) 来获取该 View 在适配器中位置;

这个方法中比较关键的就是第一点的理解,设置 outRect 的值,效果类似给 item 设置了 padding 或者 margin。我们可以看一下下图理解一下:
2.png
到这其实我们已经可以看到,RecycleView 是通过这个 rectRect 的值来给 item 设置“空隙”,然后达到分割线的效果。

为了更好的理解这个方法,我们在代码中动态设置 outRect 的四个值:

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
public class BasicDivider extends RecyclerView.ItemDecoration {

private static final String TAG = "DividerItem";
/**
* 获取 item 在四个方向上的偏移量
*/
private int leftOffset;
private int topOffset;
private int rightOffset;
private int bottomOffset;
private Context context;

public BasicDivider(Context context) {
this(context,0,0,0,0);
}

public BasicDivider(Context context, int left, int top, int right, int bottom) {
this.leftOffset = left;
this.topOffset = top;
this.rightOffset = right;
this.bottomOffset = bottom;
this.context = context;
}
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
}

@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(leftOffset,topOffset,rightOffset,rightOffset);
}

其中的 leftOffset、topOffset、rightOffset、rightOffset 由外部设置得到,这里代码省略,然后看下效果:

需要注意的是通过 getItemOffsets 方法设置分割线,分割线的颜色是 RecycleView 的 backgroundColor 的颜色。所以我们如果想修改分割线的颜色,就必须修改 recycleView 的背景色。

那么假设我想要一个渐变色的分割线或者其他样式的分割线怎么办呢?
答案就是剩下的两个方法 ondraw()和 onDrawOver()方法 可以上场了。

2.onDraw () 方法:

Draw any appropriate decorations into the Canvas supplied to the RecyclerView. Any content drawn by this method will be drawn before the item views are drawn, and will thus appear underneath the views
在 RecycleView 的画布中绘制任意合适的装饰。通过该方法绘制的内容将在 item 的绘制之前被绘制,因此会在 item 的下方显示

通过这个介绍我们可以知道以下三点:

  • onDraw() 方法的使用对象是整个 RecycleView,而不是针对单个的 item
  • onDraw()方法可以绘制任何装饰( 通过canvas可以随心所欲)
  • 这个方法在 item 之前被绘制,因此位置计算不当,可能会出现被 item 遮挡的情况

那我们来想一下,如何实现在 RecycleView 背景为灰色,但分割线为 “10 px 的红色” 的功能。这里我写下我分析的步骤:

(1).通过 getItemOffsets()方法设置 item 的间距,预留分割线的宽度
(2).通过 onDraw()方法绘制分割线,具体又细分为:

  • 计算绘制的位置。因为canvas 是这个 RecycleView 的,所以我们要计算每条线的位置
  • 绘制。使用 canvas.drawRect()或者 canvas.drawLine() 方法进行绘制

看一下主要代码实现:

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
// 在提供给 RecycleView 的画布中绘制适当的装饰。
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
Log.d(TAG,"----onDraw---");
canvas.save();
final int left;
final int right;
left = 0;
right = parent.getRight();
final int childCount = parent.getChildCount();
bottomOffset = 10;
// 遍历 item,获取每个item 的位置从而定位要绘制的分割线的位置
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
//计算分割线的位置
final int bottom = child.getBottom()+bottomOffset;
final int top = child.getBottom();
canvas.drawRect(left,top,right,bottom,paint);
}
canvas.restore();
}

//.重新得到给定 item 的任何偏移量
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//设置 item 底部偏移 10 dp
outRect.bottom = 10;
}

关于分割线位置的计算,我们可以借助一下图,就会很容易计算:
3.png
只要计算 A点和 D 点的坐标即可。当然,我这里写的是最简单的情况,不考虑 RecycleView 及 item 的 padding 和 margin,其实计算的时候也是应该考虑进去的。
然后我们运行看一下效果:
4.png
然后来验证一下,onDraw()方法是在 item 之前绘制的,把上述 onDraw()方法中的 bottomOffset = 50,也就是绘制的区域其实是 item 下方 50 px 高度的矩形。

然后运行,发现效果和上一张图一样。也就是说我们绘制的内容被 item 给覆盖住了。

所以,如果想让内容绘制在 item 之上,不被盖住,我们需要了解下面的方法。

3.onDrawOver () 方法:
与 onDraw() 方法作用一样,但是是在 item 之后绘制,所以不会被遮挡覆盖。

我们把例子中 onDraw()方法中的代码移到 onDrawOver () 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
Log.d(TAG,"----onDrawOver---");
canvas.save();
final int left;
final int right;
left = 0;
right = parent.getRight();
bottomOffset = 50;
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int bottom = child.getBottom()+bottomOffset;
final int top = child.getBottom();
canvas.drawRect(left,top,right,bottom,paint);
}
canvas.restore();
}

然后看一下效果:
5.png
可以看到,方法中绘制的内容会覆盖在 item 之上。也就验证了 onDrawOver () 方法在 item 之后绘制。

二.解析默认分割线的实现

了解了 ItemDecoration 各个方法的作用,下面我们来分析一下默认提供的分割线的内部实现,

先看一下代码:

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
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;

private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };

private Drawable mDivider;

/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
private int mOrientation;

private final Rect mBounds = new Rect();

/**
* Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
* {@link LinearLayoutManager}.
*
* @param context Current context, it will be used to access resources.
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
setOrientation(orientation);
}

/**
* Sets the orientation for this divider. This should be called if
* {@link RecyclerView.LayoutManager} changes orientation.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}

/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}

private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}

final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}

private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}

final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}

1.首先来分析一下 getItemOffsets() 方法:

  • mDivider 在构造函数中初始化值,为默认的分割线的 drawable 属性值
  • 判断 LayoutManger 的方向,如果是 Vertical,就设置 item 的 bottom 偏移默认分割线的高度,如果是 Horizontal 就设置 item 的 right 偏移默认分割线的宽度

2,分析一下 onDraw() 方法
首先进行 LayoutManger 方向的判断,不同的方向,分割线位置计算不同,我们这里就拿最常用的 Vertical 时的状态来看,所以具体看一下 drawVertical()方法:

  • 首先通过 parent.getClipToPadding() 判断 RecycleView 是否设置这个参数为 true (默认值),如果为 true,那 RecycleView 显示位置区域就不会包含 padding 值的位置,所以需要进行裁剪,否则,显示 padding 区域,也就是 RecycleView 控件本身占的区域
  • 遍历 item
  • 获取 item 的 坐标位置,包含 item 的 insets 设置的偏移值(比如通过 getItemOffsets 设置的)和 margin 值。
  • 计算绘制区域的位置信息

可以看到,官方代码是先得到一个 drawable 对象,然后计算出 item 所以的偏移值,包括我们通过 getItemOffsets 设置的分割线的值,先计算出 bottom,然后根据 bottom 计算 top。最后通过 onDraw() 设置 drawable 的位置来达到分割线的效果。
无疑这种考虑的更全面,我的 demo 是默认 RecycleView 和 item 都没有偏移量的情况。

所以,可以看到,绘制分割线,分割线位置的计算是比较重要的,需要具体情况具体分析。

三. 利用 shape 设置分割线

不知道大家有没有注意到刚刚看的默认分割线的类中,暴露了这样一个方法:

1
2
3
4
5
6
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}

既然是通过 mDivider.draw(canvas); 最后来绘制分割线的,那么只要我们修改了 drawable 对象,分割线的样式也会修改。

所以,对于简单的分割线,我们可以在 shape 文件中设置一个分割线的样式,然后通过默认分割线的 setDrawable()方法设置。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
 DividerItemDecoration itemDecoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
itemDecoration.setDrawable(getResources().getDrawable(R.drawable.shape_divider));
rvCommon.addItemDecoration(itemDecoration );

shape:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#000"
/>
<size android:height="@dimen/space_10"/>
<stroke android:color="@color/colorPrimaryDark"
android:width="@dimen/space_2"/>
</shape>

效果:
6.png

总结

其实分割线的实现很简单,主要掌握 ItemDecoration 内容就好。大致步骤有三点:

1. 通过 getItemOffset() 方法设置 item 的偏移量
2. 在 onDraw()或 onDrawOver() 方法中完成绘制
2.1 遍历 item,计算分割线的位置
2.2 通过 draw()方法完成绘制

其中需要注意的点有:

  • getItemOffset()方法作用于 item
  • onDraw()或 onDrawOver() 方法作用于 RecycleView
  • onDraw()先于 item 绘制
  • onDrawOver() 后于 item 绘制
  • 计算分割线位置时需考虑 RecycleView 和 item 自身偏移量的问题

参考

https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration

RecycleView 系列(1)之 基本使用-数据展示

发表于 2019-01-13 | 分类于 android | 阅读次数: ℃

前言

RecycleView 这个控件出来的时间已经很久了。然而我项目中开始从 ListView 彻底转型 RecycleView 也是从刚刚过去的18 年的时候才开始。不管是因为时间的原因也好,还是因为本身不太接受新的东西也好,我之前做项目的时候能用 ListView 就用 ListView,毕竟熟悉的知识会让自己有安全感。但是就像当初不太接受 android studio 而用 eclipse 一样,时间和实践都会让我们做出正确的选择。

因为项目中都只是用了 RecycleView 最基本的知识,而在 19 年,我想更了解它。所以开始了第一个 flag 的攻克。虽然网上已经有很多很棒的 RecycleView 系列文章,但我还是希望自己亲自一步一个脚印地去探索。通过一个个列子来慢慢了解 RecycleView 中的各个方法,努力做到知其然知其所以然。

一.认识 RecycleView

RecycleView 是在 api 版本 22 中引入的。官方文档中是这样介绍它的:

A flexible view for providing a limited window into a large data set.
用有限的窗口展示大量数据集的一个灵活的控件。

而我们看一下之前一直用的 ListView 的 介绍:

Displays a vertically-scrollable collection of views, where each view is positioned immediatelybelow the previous view in the list. For a more modern, flexible, and performant approach to displaying lists, use RecyclerView.
显示可垂直滚动的视图集合,其中每个视图立即位于列表中前一个视图的下方。想展示一个更现代、更灵活和性能更好的列表的话,请使用 RecycleView。

所以,从这两个介绍来看,我们知道 RecycleView 比 ListView 更灵活、性能更好。其实我们可以先来对比一下两者的继承关系:
1.png
可以看到 RecycleView 是直接继承自 ViewGroup的,比 ListView 直接少了两级的继承关系(嗯,如果按辈分的话,好像是爷爷辈,哈哈),是不是也可以看得出 RecycleView 比 ListView 更灵活?

好了,来看看如何使用吧。

二.使用

1.在 module 的 build.gradle 中引入依赖:

implementation ‘com.android.support:recyclerview-v7:27.0.0’

这个版本号可以根据自己的 sdk 的版本来。也可以通过这里看看都有哪些版本。

2.布局文件中引入

1
2
3
4
5
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_home"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>

3.创建 Adapter

就像使用 ListView 一样,我们需要为 RecycleView 创建一个 Adapter,这个大家估计现在已经很熟了。

所以我们新建一个类,继承 RecyclerView.Adapter,同时创建 ViewHolder,实现三个默认的抽象方法,三个方法及含义如下:

方法 含义
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) 实例化一个 ViewHolder,并初始化一些 RecycleView 使用的私有字段
void bindViewHolder (VH holder,int position) 更新 RecycleView 指定位置的 item 的内容,并初始化一些 RecycleView 使用的私有字段
int getItemCount() 返回适配器所持有的数据集中 items 的总数

代码如下:

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
public class CommonRvAdapter extends RecyclerView.Adapter<CommonRvAdapter.ViewHolder> {
private final Context context;
private List<CommonDataBean> items;

/**
* 构造函数
* @param items
* @param context
*/
public CommonRvAdapter(List<CommonDataBean> items, Context context) {
this.items = items;
this.context = context;
}

/**
* 实例化 ViewHolder
* @param parent
* @param viewType
* @return
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_home, parent, false);
return new ViewHolder(v);
}

/**
* 更新 View 内容
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
CommonDataBean item = items.get(position);
holder.tvItem.setText(item.getData());
holder.llLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener !=null){
onItemClickListener.onItemClick(view,position);
}
}
});

holder.llLayout.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (onItemLongClickListener !=null){
onItemLongClickListener.onItemLongClick(view,position);
}
return true;
}
});
}

/**
* 获取 item 的总数
* @return
*/
@Override
public int getItemCount() {
if (items == null) {
return 0;
}
return items.size();
}

class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_item)
TextView tvItem;
@BindView(R.id.ll_layout)
LinearLayout llLayout;
public ViewHolder(@NonNull View itemView) {
super(itemView);
ButterKnife.bind(this,itemView);
}
}

}

4.设置 RecycleView

1
2
3
LinearLayoutManager manager = new LinearLayoutManager(this);
recycleView.setLayoutManager(manager );
recycleView.setAdapter(adapter);

可以看到这里我们比起设置 LisView 来,我们多设置了一个 LinearLayoutManager。这里也是体现 RecycleView 比较灵活的一个点。它没有规定布局的排列样式,提供了一个 LayoutManager ,只要去修改 LayoutManager ,就可以修改布局的样式。我们这里实现 LinearLayoutManager ,类似 ListView 的一个样式。剩余的关于 LayoutManager 后面单独再说。

好了,我们来看一下设置后的效果:
2.gif

5. 添加点击事件
RecycleView 并没有像 ListView 那样直接提供 item 的行点击事件。所以需要我们自己实现,也很简单,在 adapter 中定义点击事件的接口。然后在 Activity 中实现这个接口即可。
adapter 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 事件监听
*/
private onItemClickListener onItemClickListener;
private onItemLongClickListener onItemLongClickListener;

public interface onItemClickListener{
void onItemClick(View view, int position);
}
public interface onItemLongClickListener{
void onItemLongClick(View view, int position);
}

public void setOnItemClickListener (onItemClickListener onItemClickListener){
this.onItemClickListener = onItemClickListener;
}
public void setOnItemLongClickListener (onItemLongClickListener onItemLongClickListener){
this.onItemLongClickListener = onItemLongClickListener;
}

activity 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
commonRvAdapter.setOnItemClickListener(new CommonRvAdapter.onItemClickListener() {
@Override
public void onItemClick(View view, int position) {
Toast.makeText(BasicRvActivity.this, "点击" + dataBeanList.get(position).getData() , Toast.LENGTH_SHORT).show();
}
});
commonRvAdapter.setOnItemLongClickListener(new CommonRvAdapter.onItemLongClickListener() {
@Override
public void onItemLongClick(View view, int position) {
Toast.makeText(BasicRvActivity.this, "长按" + dataBeanList.get(position).getData() , Toast.LENGTH_SHORT).show();

}
});

再来看下效果:
3.gif

总结

好了,我们已经完成了基本的数据展示,非常的简单。本来是想跳过这一节的,但是还是记录一下的好,虽然简单,但还是得一步步的来,防止自己眼高手低。

不过好像缺了点装饰,比如分割线。那我们下篇博客就来探索一下!

demo 地址: 点这里

参考

  • https://developer.android.com/reference/android/support/v7/widget/RecyclerView
  • https://developer.android.com/reference/android/widget/ListView

React Native -- 布局位置随输入框变化的问题处理

发表于 2019-01-13 | 分类于 react native | 阅读次数: ℃

前言

在之前项目的开发过程中,下图这样的需求很是常见:
1.gif
当键盘弹起时,某个布局正好在键盘之上,当键盘消失时,这个布局又回到页面最底部。

今天来讨论的就是这个功能我用过的方法及踩过的坑。

方案1

首先我们观察到下面的一块布局(以下简称 “A” 布局)是随着键盘的弹起和消失来改变位置的,并且距离屏幕的底部正好是键盘的高度。
所以,第一个思路,设置 A 布局的位置方式为绝对定位,并且是计算距离父布局底部的位置为键盘的高度。
代码中定义一个展示键盘高度的变量 keyboardHeight ,然后设置键盘显示与消失的事件监听,并给 keyboardHeight 变量赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
componentDidMount(){
this.keyboardDidShow = Keyboard.addListener('keyboardDidShow', this.keyboardShow.bind(this));
this.keyboardDidHide = Keyboard.addListener('keyboardDidHide', this.keyboardHide.bind(this));
}

keyboardShow(e){
this.setState({
keyboardHeight:e.endCoordinates.height
});
};
keyboardHide(){
this.setState({
keyboardHeight:0
});
}

然后在 render 方法中设置 A 布局的样式:

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
render() {
return (
<View style = {{flex:1}}>
<TextInput
style={{height: 140, borderColor: '#999', borderWidth: 1,margin:15}}
onChangeText={(text) => this.setState({text})}
placeholder={'随便说点什么吧'}
textAlignVertical = {'top'}
/>

<View style = {[styles.bottomView,{bottom:this.state.keyboardHeight}]}>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginLeft:15}} resizeMode ={'contain'}source = {require('../img/keyBoard.png')}/>
</TouchableOpacity>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginRight:15}} resizeMode ={'contain'}source = {require('../img/emoji.png')}/>
</TouchableOpacity>
</View>
</View>

);
}

const styles = StyleSheet.create({

bottomView:{
position:'absolute',
flexDirection:'row',
height:48,width:'100%',
backgroundColor:'#DD4F43',
justifyContent:'space-between',
alignItems:'center'
}
});

然后,我们来看一下效果:
2.gif

咦,好像翻车了。
可以看到当键盘没有弹出的时候是好的,但是当键盘弹出后 A 布局就不见了。那这又有两种情况,一种是 A 布局在正确位置的上方,超出屏幕了,一种是在正确位置的下方,被键盘遮挡没弹起来。
那我们来将 A 布局的位置改为 keyboardHeight -150

看下效果:
3.gif
嗯,A 布局出现了,并且是在键盘的上方。由此可见我们的第一个猜想是对的。
那我们不妨再大胆做个猜想,A 布局偏离正确位置的高度会不会就是一个键盘的高度呢?如果是这样的话,那我们 A 布局的 bottom 就应该为 keyboardHeight - keyboardHeight 即为 0!

改下代码,然后来看下效果
4.gif
貌似完美! 也就是在 android 上也不需要去计算键盘的高度,布局直接在键盘之上?这个原理我还不清楚,但感觉和输入框与软键盘之间的恩怨有联系。
然后我们再看下在 ios 上的表现,嗯,这里没有测试机,就不贴图了。但事实证明会有个问题,在 ios 上如果这样设置当键盘显示的时候,A 布局不会弹起来,而是还在屏幕的底部。所以,ios 还是要计算键盘的高度。

所以,最后适配如下:

1
2
3
4
5
6
7
8
<View style = {[styles.bottomView,{bottom:Platform.OS == 'ios'? this.state.keyboardHeight:0}]}>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginLeft:15}} resizeMode ={'contain'}source = {require('../img/keyBoard.png')}/>
</TouchableOpacity>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginRight:15}} resizeMode ={'contain'}source = {require('../img/emoji.png')}/>
</TouchableOpacity>
</View>

在这种方案实行了俩星期后,测试就提出来严重的兼容性问题:

  • 在某些 android 手机上(如魅蓝),键盘弹起时,A 布局会距离键盘上方大约一个键盘的高度,即弹的太上了
  • 在某些 android 手机 或者 iPhoneX 上,键盘弹起时 A 布局弹不出来

这两个问题都是偶现的,但在某些机型上的偶现率达 50%
所以,看来方案一是有问题。
但问题究竟出在哪里,我没弄明白,但敢肯定还是和输入框与软键盘的适配有关,曾经尝试设置 activity 的 softnput 参数,未果。

方案2

那我们就来换种思路,方案一我们是采用基于 bottom 的定位。但也许由于软键盘与输入框的特殊关系,这个 bottom 的值在不同机型上表现不同。那我们能否改为基于 top 定位呢。
分析一下,如果是基于 top 定位的情况,值应该怎么算,来看一下分析图:
5.png

  • 当键盘没有弹起的时候,A 布局距离父布局的顶部的距离是 h- bottomHeight,即父布局高度减去 A 布局自身高度。
  • 当键盘弹起时,A 布局距离父布局的顶部的距离是 h- bottomHeight-keyboardHeight,即多减掉键盘的高度。

    所以最终计算方式为:父布局的高度 - A 布局自身的高度 - 键盘的高度

    代码中,我们首先需要计算一下父布局的高度,通过 onLayout 方法计算出。然后设置 A 布局的 top 属性值

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
_onLayout(event) {
//获取根View的宽高,以及左上角的坐标值
this.setState({
layoutHeight : 1
})
}
render() {
return (
<View style = {{flex:1}} onLayout={()=>{this._onLayout}}>
<TextInput
style={{height: 140, borderColor: '#999', borderWidth: 1,margin:15}}
onChangeText={(text) => this.setState({text})}
placeholder={'随便说点什么吧'}
textAlignVertical = {'top'}
/>

<View style = {[styles.bottomView,{top:520-48-this.state.keyboardHeight}]}>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginLeft:15}} resizeMode ={'contain'}source = {require('../img/keyBoard.png')}/>
</TouchableOpacity>
<TouchableOpacity onPress = {()=>{alert(1)}}>
<Image style = {{width:26,height:26,marginRight:15}} resizeMode ={'contain'}source = {require('../img/emoji.png')}/>
</TouchableOpacity>
</View>
</View>

);
}

刷新一下,看一下效果:
6.gif

貌似没什么毛病,通过在其他机型和 ios 上测试,目前没有发现问题。

所以第二种方案可行。

总结

以后遇到这种需求,最好要避免根据 bottom 来定位,可以换种思路,根据 top 来定位。

android -- String 格式化使用示例

发表于 2019-01-13 | 分类于 android | 阅读次数: ℃

前言

这篇文章已经拖了快一个月。。虽然简单,还是总结一下吧。

最近做项目的时候遇到群发短信消息的需求,然后有个短信模板,大致格式如下 :

{userName} 在 {appName} 中对您发布了新的消息 :{message} 快去查看吧。

也就是在创建短信消息体的时候会有 3 个变量 ,然后你估计会问,这有什么问题? 嗯,确实没啥大问题。
所以我们这里来讨论的,是对于这个消息体字符串的创建我们有哪些方法,而哪个方法又显得比较优雅。

字符串拼接的方法

1、首先最原始、最粗暴的方法,用 ”+“ 号拼接

1
2
3
4
private  String  createMessageBody(String userName , String appName,String message  ){ 
String messageBody = userName + "在" + appName + "中对您发布了新的消息 :" + message + " 快去查看吧。";
return messageBody;
}

貌似也没什么不好的地方,但这么多加号拼接在一起,恩, 有种 low low 的感觉

2、StringBuilder 拼接

1
2
3
4
5
6
7
8
9
10
private  String  createMessageBody(String userName , String appName,String message  ){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(userName)
.append("在")
.append(appName)
.append("中对您发布了新的消息 :")
.append(message)
.append("快去查看吧 。");
return stringBuilder.toString();
}

恩,这种方法貌似看起来也比较臃肿,可读性也不好。只是比第一种效率高,不会产生那么多对象。

3、字符串模板拼接

1
2
3
4
5
6
xml 中 :  <string name="message_body">%1$s在%2$s中对您发布了新的消息:%3$s。快去查看吧。</string>

private String createMessageBody(String userName , String appName,String message ){
String messageBody = getResources().getString(R.string.message_body,userName,appName,message);
return messageBody;
}

可以看到这种方法是需要首先在 string 文件夹下定义好完整的文字,然后在代码中通过 getString 方法,传入条件参数。
很显然,这种方法更符合开发规范,因为我们一般是要尽量避免在代码中直接明文写字符串的。

还有,对于支持多语言的应用,前两种方法就更不值得用了。

然后,我们来看一下 android 中 getString(@StringRes int id, Object... formatArgs) 这个方法内部是怎么实现的:

1
2
3
4
5
6
@NonNull
public String getString(@StringRes int id, Object... formatArgs) throws NotFoundException {
final String raw = getString(id);
return String.format(mResourcesImpl.getConfiguration().getLocales().get(0), raw,
formatArgs);
}

可以看到,最终调用的是 java 中 String 的 format 方法。

Java String 格式化

这个本来想详细列举一下,但是发现有一篇译文写得很详细,可以作为速查表:
isea533 – Java String 格式化示例

这里简单贴一下文章中常用的格式说明符:
20181225175236121.jpg
剩余的这里就不再讲解。。。直接结束

总结

恩,这就结束了(😂)

看起来像一篇水文(尴尬)

所以以后遇到需要字符串格式化或者拼接的时候可以优先考虑一下 String.format 方法。来减少直接在代码中输入文字。

PS : 前两天看的 kotlin 的字符串模板挺好用的,但是也相当于是直接硬编码了。

参考文章

isea533 – Java String 格式化示例

android -- EditText 设置 imeOptions 属性为何失效?

发表于 2018-12-03 | 分类于 android | 阅读次数: ℃

前言

最近改 bug 的时候碰到一个小知识点,在搜索界面希望键盘上的 enter 键改为搜索按钮。也就是下图的效果,是不是非常常见。
在这里插入图片描述

然后我就记得 Editext 有个 imeOptions 的属性,可以设置 enter 键的效果。所以果断在 xml 中写下 android:imeOptions="actionSearch",然后把问题改为已修复,信心满满。结果等编译运行起来在手机上发现没有起作用,还是 enter 键。 搜索了一下,发现大家都是这样回答的:

如果设置了 imeOptions 属性,键盘没有变化 ,那么需要单独加一些其他的属性配合使用,xml中 属性设置:
1 将singleLine设置为true
2 或者将inputType设置为text

试了一下,果然 ok 了。但为什么这样设置显示就对了呢?你是不是同样也有疑问? 所以就让我们共同去探寻答案吧!

ImeOptions 属性源码解析

这里首先推荐一个看源码的插件:AndroidSourceView。

Step1

因为如果是在 JAVA 文件中,我们设置 imeOptions 属性代码为: editText.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
所以首先要定位到 setImeOptions() 这个方法 ,在 EdiText 中没有搜到,所以果断跳到 EditText 的父类 TextView 的源码中,然后发现目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Change the editor type integer associated with the text view, which
* is reported to an Input Method Editor (IME) with {@link EditorInfo#imeOptions}
* when it has focus.
* @see #getImeOptions
* @see android.view.inputmethod.EditorInfo
* @attr ref android.R.styleable#TextView_imeOptions
* 当输入法编译器(IME)获取焦点的时候,通过{@link EditorInfo#imeOptions} 报告给输入法编辑器,
* 来更改与文本视图关联的编辑器类型值。
*/
public void setImeOptions(int imeOptions) {
//1.判断是否需要创建 Editor 对象
createEditorIfNeeded();
//2.判断是否需要创建 InputContentType
mEditor.createInputContentTypeIfNeeded();
//3. 将入参赋值给InputContentType.imeOptions
mEditor.mInputContentType.imeOptions = imeOptions;
}

这个方法里面只是进行了判断是否需要创建一些对象,然后将我们的入参赋值给 InputContentType.imeOptions。从这方法的注释中我们可以知道关键对象是 EditorInfo#imeOptions 。

Step2

继续搜索关键字 imeOptions ,然后发现下面这个方法:

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
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
mEditor.createInputMethodStateIfNeeded();
outAttrs.inputType = getInputType();
if (mEditor.mInputContentType != null) {
outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
outAttrs.actionId = mEditor.mInputContentType.imeActionId;
outAttrs.extras = mEditor.mInputContentType.extras;
outAttrs.hintLocales = mEditor.mInputContentType.imeHintLocales;
} else {
outAttrs.imeOptions = EditorInfo.IME_NULL;
outAttrs.hintLocales = null;
}
if (focusSearch(FOCUS_DOWN) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
}
if (focusSearch(FOCUS_UP) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
}
if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
== EditorInfo.IME_ACTION_UNSPECIFIED) {
if ((outAttrs.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
// An action has not been set, but the enter key will move to
// the next focus, so set the action to that.
outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
} else {
// An action has not been set, and there is no focus to move
// to, so let's just supply a "done" action.
outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
}
if (!shouldAdvanceFocusOnEnter()) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
}
if (isMultilineInputType(outAttrs.inputType)) {
// Multi-line text editors should always show an enter key.
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
outAttrs.hintText = mHint;
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
return ic;
}
}
return null;
}

然后我们只关心我们探究的信息,所以伪代码如下:

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
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
// return mEditor == null ? EditorInfo.TYPE_NULL : mEditor.mInputType
outAttrs.inputType = getInputType();
if (mEditor.mInputContentType != null) {
outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
} else {
outAttrs.imeOptions = EditorInfo.IME_NULL;
}
if (focusSearch(FOCUS_DOWN) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
}
if (focusSearch(FOCUS_UP) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
}
if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
== EditorInfo.IME_ACTION_UNSPECIFIED) {
//..代码省略
}
if (isMultilineInputType(outAttrs.inputType)) {
// Multi-line text editors should always show an enter key.
//多行文本编译器总是会显示 enter 键
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
return null;
}
Step3

我们首先分析一下:

  • 第一个 if 判断: mEditor.mInputContentType 这个值是否为空,答案是 false ,因为我们在setImeOptions 方法的第 3 步已经给它赋值了,所以不会为空,所以outAttrs.imeOptions 结果为我们设置的值
  • 第二个 if 判断:是来寻找我们的 Editext 对象的往下的一个方向是否存在可以获取焦点的 View,这个得看实际输入框下面布局是否还会有一个输入框,目前项目没有,所以 false
  • 第三个 if 判断:跟第二个条件一样,只过是方向为向上查找
  • 第四个 if 判断:首先 (outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION值是多少呢,我们代码里设置的值是 IME_ACTION_SEARCH ,值为 3,EditorInfo.IME_MASK_ACTION 的值为 255 ,取二进制值为 00000011 & 1111 1111 = 00000011 十进制为 3,而EditorInfo.IME_ACTION_UNSPECIFIED 值为 0,所以结果为 false。

    到目前为止 outAttrs.imeOptions 的结果依然为我们在代码中设置的值。

    Step4

    来看最后一个 if 判断:isMultilineInputType(outAttrs.inputType) 光看字段意思我们能猜到判断输入框是不是多行输入。看一下具体代码:

1
2
3
4
private static boolean isMultilineInputType(int type) {
return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
}

type 这个值为我们在 onCreateInputConnection() 方法中通过getInputType () 方法获取,结果为返回我们代码中设置的 inputType 值,而我们代码中没有设置,那么这个 inputType 的默认值是什么呢?源码中没有找到,然后我自己 debug 了一下,结果如下图:
在这里插入图片描述
所以默认值为 131073,而 InputType 各种类型的值如下:

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
public interface InputType {
int TYPE_CLASS_DATETIME = 4;
int TYPE_CLASS_NUMBER = 2;
int TYPE_CLASS_PHONE = 3;
int TYPE_CLASS_TEXT = 1;
int TYPE_DATETIME_VARIATION_DATE = 16;
int TYPE_DATETIME_VARIATION_NORMAL = 0;
int TYPE_DATETIME_VARIATION_TIME = 32;
int TYPE_MASK_CLASS = 15;
int TYPE_MASK_FLAGS = 16773120;
int TYPE_MASK_VARIATION = 4080;
int TYPE_NULL = 0;
int TYPE_NUMBER_FLAG_DECIMAL = 8192;
int TYPE_NUMBER_FLAG_SIGNED = 4096;
int TYPE_NUMBER_VARIATION_NORMAL = 0;
int TYPE_NUMBER_VARIATION_PASSWORD = 16;
int TYPE_TEXT_FLAG_AUTO_COMPLETE = 65536;
int TYPE_TEXT_FLAG_AUTO_CORRECT = 32768;
int TYPE_TEXT_FLAG_CAP_CHARACTERS = 4096;
int TYPE_TEXT_FLAG_CAP_SENTENCES = 16384;
int TYPE_TEXT_FLAG_CAP_WORDS = 8192;
int TYPE_TEXT_FLAG_IME_MULTI_LINE = 262144;
int TYPE_TEXT_FLAG_MULTI_LINE = 131072;
int TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288;
int TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32;
int TYPE_TEXT_VARIATION_EMAIL_SUBJECT = 48;
int TYPE_TEXT_VARIATION_FILTER = 176;
int TYPE_TEXT_VARIATION_LONG_MESSAGE = 80;
int TYPE_TEXT_VARIATION_NORMAL = 0;
int TYPE_TEXT_VARIATION_PASSWORD = 128;
int TYPE_TEXT_VARIATION_PERSON_NAME = 96;
int TYPE_TEXT_VARIATION_PHONETIC = 192;
int TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 112;
int TYPE_TEXT_VARIATION_SHORT_MESSAGE = 64;
int TYPE_TEXT_VARIATION_URI = 16;
int TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 144;
int TYPE_TEXT_VARIATION_WEB_EDIT_TEXT = 160;
int TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS = 208;
int TYPE_TEXT_VARIATION_WEB_PASSWORD = 224;
}

结合 stackoverflow 上 Default inputType of EditText in android 答案和 InputType 的值,也就是说 inputType 默认值 为 InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_FLAG_MULTI_LINE 组合的结果。

然后我们继续往下看,isMultilineInputType() 方法的返回值

1
2
(type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE

转换为十进制为:(131073 & (15 | 131072)) == 1 | 131072
转换为二进制为:(100000000000000001 &(1111 | 100000000000000000)) == 1 | 100000000000000000
即 100000000000000001 == 100000000000000001
即 true。

所以会进入最后一个条件中,而该返回值明确指出 多行文本编译器总是会显示 enter 键 。所以我们会看到我们设置的属性失效了。

而如果我们设置了 singleLine = true ,源码中是这样设置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
* @param singleLine
*/
private void setInputTypeSingleLine(boolean singleLine) {
if (mEditor != null
&& (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
== EditorInfo.TYPE_CLASS_TEXT) {
if (singleLine) {
mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
} else {
mEditor.mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
}
}
}

如果 singleLine 为 true,inputType = ~ 131072,即011111111111111111 ,最后计算结果为 111111111111111111 == 100000000000000001,显然 false。
或者设置了inputType = TYPE_CLASS_TEXT 我们也可以得出返回值为 false,所以不会进判断条件,也就是 imeOptions 的值就是我们在代码里或者 xml 中设置的。

总结

所以要让 imeOptions 起作用,关键是不能满足 可以多行输入的属性。
有两种处理方案:

1. 设置 singleLine 属性为 true
2. 设置 inputType 属性。 而这个属性值不一定非要是 ‘text’ 即TYPE_CLASS_TEXT 类型,只要最后结果不是 131072 (也就是 InputType.TYPE_CLASS_TEXT |InputType.TYPE_TEXT_FLAG_MULTI_LINE 即可,根据需求来定是 设置 text 或者 纯数字键盘等等)

参考文章

EditorInfo文档
EditText中imeOptions属性使用及设置无效解决
在线进制转换

React Native --踩坑记 之 创建指定 React Native版本的项目

发表于 2018-12-03 | 分类于 react native | 阅读次数: ℃

前 言

最近一段时间一直在写 RN 的项目,期间遇到了挺多的坑,然后想着记录一下填坑的过程(想看答案的小伙伴可以忽略我的心厉路程,直接跳到结尾总结处)。

step1. 我竟然偷偷的给自己挖了个坑?

于是乎,第一步,赶紧新建一个demo,飞快地在 terminal 中输入 react native init yx_rnDemo ,漫长的等待后,项目成功建立。 然后用 IDE 打开 demo ,执行react-native run-android 命令,结果半路夭折,没跑起来。仔细一看错误日志,发现 android 各种依赖都下载失败。然后看了下 package.json 中 react-native 的版本,发现引用的是最新版本,然后点击查看 android 文件夹,发现引用的 gradle 版本是 3.1.4 ,然鹅我用的还是 2.3.3 的版本。。

因为比较懒(这句话在我的博客中出现的次数不低,懒是万恶之源,罪过罪过~~),不想升级,再配置一系列东西,所以按照 中文网 给出的创建指定版本的方法:

提示:你可以使用–version参数(注意是两个杠)创建指定版本的项目。例如react-native init MyApp –version 0.44.3。注意版本号必须精确到两个小数点

删掉 demo ,重新输入 react native init yx_rnDemo --version0.47.2 ,结果最后发现其实创建的还是最新版。。(内心 OS,what??其实细心的朋友估计已经发现问题了,哈哈,嘘~~)

然后开始各种面向搜索引擎,发现大家都是这样创建的啊,并且 RN 官网上给出的命令也是这样的,为什么别人没有问题,到我这就有问题了呢。

然后换了一个命令执行: react-native init yx_rnDemo --verbose --version 0.47.2 想来看一下创建项目的详细信息,结果最后显示创建的竟然是对的!! 就是 0.47.2 。

难道说加了一个 --verbose 条件就能创建成功了?不对啊,我看了下说 --verbose 条件只是会输出详细信息的啊,照理说不应该对结果产生什么影响的。然后不信邪的我把 --verbose 命令去掉,又执行了react-native init yx_rnDemo --version 0.47.2,过了一会发现,竟然也是对的!!

吓得我赶紧去翻我第一次写的命令,一对比,发现我第一个命令--verison 后没有换行~~

第一次的:react native init yx_rnDemo –version0.47.2
第二次的:react-native init yx_rnDemo –version 0.47.2

本来到这就可以结束的,然而作为一个想有灵魂的程序猿,还是很想弄清楚为什么不加空格就会创建最新版本,而不是提示语法错误的原因。

step2. 一步步分析坑是如何产生的

这里首先介绍一下, react-native 源码中 react-native-cli 文件夹下的 index.js 这个文件很重要,它唯一的工作是初始化存储库,然后将所有命令转发到本地的 react-native 版本。所以我们初始化项目时做的操作可以在这个文件中找到。

打开这个 js 文件,然后开始一探究竟吧。
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
'use strict';

var fs = require('fs');
var path = require('path');
var exec = require('child_process').exec;
var execSync = require('child_process').execSync;
var chalk = require('chalk');
var prompt = require('prompt');
var semver = require('semver');
/**
* Used arguments:
* -v --version - to print current version of react-native-cli and react-native dependency
* if you are in a RN app folder
* init - to create a new project and npm install it
* --verbose - to print logs while init
* --template - name of the template to use, e.g. --template navigation
* --version <alternative react-native package> - override default (https://registry.npmjs.org/react-native@latest),
* package to install, examples:
* - "0.22.0-rc1" - A new app will be created using a specific version of React Native from npm repo
* - "https://registry.npmjs.org/react-native/-/react-native-0.20.0.tgz" - a .tgz archive from any npm repo
* - "/Users/home/react-native/react-native-0.22.0.tgz" - for package prepared with `npm pack`, useful for e2e tests
*/

var options = require('minimist')(process.argv.slice(2));

文件的前 62 行都是在声明变量及引用,其中有几个变量这里我们需要知道它们是的作用:

变量名 含义
fs Node.js 中 文件系统操作的模块
path Node.js 中用于处理文件路径的小工具的模块
exec Node.js 中子进程模块, 衍生一个 shell 并在 shell 上运行命令
execSync exec 的同步函数,会阻塞 Node.js 事件循环
chalk 定制控制台日志的输入样式的一个插件
prompt node 命令行输入控件
semver semver 语义化版本号
options 轻量级的命令行参数解析工具

其中一个很关键的变量 options ,也就是引用的require('minimist')(process.argv.slice(2)) ,是一个命令行参数解析工具,具体的介绍可以参考这里,它是以键值对进行解析的。比如我们输入的命令行是:react-native init yx_rnDemo --version 0.47.2 ,其中 --version 0.47.2 就是一个可解析的键值对,key 为 version , value 为 0.47.2 。

这个文件中,我们有用到的键值对的值在截图的注释中可以看到:

  • -v : 打印当前 react-native-cli 的版本和 react native 的依赖关系
  • init : 创建一个新工程并且执行 npm install
  • --verbose: init 时添加的参数,打印init时的参数
  • --template:用到的模板的名称
  • --version : 会覆盖默认(最新版本)安装的 react-native 的版本。 也就是如果要创建指定版本的,需要加上这个参数

OK, 各个变量的含义我们都弄清楚了,下面让我们继续探究~~

2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 switch (commands[0]) {
case 'init':
if (!commands[1]) {
console.error('Usage: react-native init <ProjectName> [--verbose]');
process.exit(1);
} else {
init(commands[1], options);
}
break;
default:
//...代码省略
break;
}
}

在这之前 116 行 定义了 commands 这个变量,取值的结果是解析的参数,应该是 _ [ 'init ', 'yx_rnDEmo'],所以会走switch 的第一个选项,去执行 init(commands[1], options) 方法,参数为 ‘yx_rnDemo’ 和 options 变量。

3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param name Project name, e.g. 'AwesomeApp'.
* @param options.verbose If true, will run 'npm install' in verbose mode (for debugging).
* @param options.version Version of React Native to install, e.g. '0.38.0'.
* @param options.npm If true, always use the npm command line client,
* don't use yarn even if available.
*/
function init(name, options) {
validateProjectName(name);

if (fs.existsSync(name)) {
createAfterConfirmation(name, options);
} else {
createProject(name, options);
}
}

很简单,先去判断我们起的工程名称是否符合命名规范,并且判断是否存在。所以下面直接看 createProject(name, options) 方法

4.

1
2
3
4
function createProject(name, options) {
//....代码省略
run(root, projectName, options);
}

这个方法里主要是去进行创建工程文件夹和 package.json 文件的操作,然后后续行动在run(root, projectName, options) 函数中

5.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function run(root, projectName, options) {
var rnPackage = options.version; // e.g. '0.38' or '/path/to/archive.tgz'

console.log('Installing ' + getInstallPackage(rnPackage) + '...');
//...代码省略
installCommand = 'npm install --save --save-exact ' + getInstallPackage(rnPackage);
if (options.verbose) {
installCommand += ' --verbose';
}
//...代码省略
try {
execSync(installCommand, {stdio: 'inherit'});
} catch (err) {
//... 代码省略
}
cli.init(root, projectName);
}

其中这个 rnPackage 就是解析的 version 参数 ,所以,对于我的第一次使用的命令:react-native init yx_rnDemo --version0.47.2 来说,解析工具并没有找到 key 为 version 的参数,所以第一次命令的 rnPackage的值应该是空的,输入正确后就是 0.47.2 了。
然后看 installCommand 这个变量,就是最终执行的命令。其中一个参数是需要到getInstallPackage(rnPackage)去确定一下是什么。

1
2
3
4
5
6
7
8
9
10
11
function getInstallPackage(rnPackage) {
var packageToInstall = 'react-native';
var isValidSemver = semver.valid(rnPackage);
if (isValidSemver) {
packageToInstall += '@' + isValidSemver;
} else if (rnPackage) {
// for tar.gz or alternative paths
packageToInstall = rnPackage;
}
return packageToInstall;
}

OMG! 看到上面的代码 激不激动,终于真相大白了!!
按照我的第一次错误的写法,这个 rnPackage 是空,然后

1
var isValidSemver = semver.valid(rnPackage);

这一行代码的含义是进行一个版本语义化规范的检查,就是你创建的版本号必须符合 semver语义化规范,也就是 x.y.z 的格式,比如 0.47.2 ,然而我现在传的空,肯定是不符合规范的,果断返回 false ,所以该方法会返回 “react-native”, 默认会安装最新版。
而我后来正确的写法,是符合规范的,最终该方法会返回 “react-native@0.47.2“ ! 然后就会下载指定的版本了。

最后我们这边可以验证下,输出的 log 参数是不是我们在代码中看到的。

上图:
在这里插入图片描述
在这里插入图片描述

果然如此~~

然后终于理解了,react-native 中文网 中提示如果创建指定版本,版本号必须满足两位小数点 这句话是为什么了。
在这里插入图片描述

step3. 总结 & 填坑

所以说了那么多,如果想在 init 时候指定版本号,非常简单,,就是官网指出的:

react-native init MyApp –version 0.44.3

但必须注意检查两点(估计也就我这么粗心的人会犯吧):

1.--version 一定要加空格,千万不要写成 --version0.44.3

2.版本号一定要两位小数点,必须符合 semver语义化规范

参考文章

https://github.com/facebook/react-native
http://nodejs.cn/api/child_process.html
http://www.runoob.com/nodejs/nodejs-path-module.html
http://nodejs.cn/api/fs.html#fs_file_system
https://www.jianshu.com/p/231b931ab389

android --巧用 flexboxLayout 布局

发表于 2018-10-24 | 分类于 android | 阅读次数: ℃

前言

某天在做项目的时候,遇到了个看似简单但又无从下手的小功能,效果类似微信群聊选择联系人界面:
1.gif
有没有发现选择人员变动后上面部分的展示是从中间往两边扩散的?最开始的时候我是直接用 RecycleView 展示的,就是一再普通不过的九宫格布局了。在产品的要求下要改成类似微信的这种布局,然后我就开始了 “面向搜索引擎”的编程,搜寻了半天,什么仿微信啊,什么中间布局啊(事实证明就算是“面向搜索引擎”的编程,我还是要学很多,因为,关键字定位的不准确),未果。

然后一想,嘿嘿,没事,轮子没有自己造呗,一看就得自定义布局。遂拿起我的小本本,准备大干一番,用笔写下思路,磨蹭了十几分钟,本子上只有几个立体几何。。。(有个小癖好,写不出来喜欢在本子上画立方体)。

然后想一时半会自定义也掌握不好啊,时间又赶,遂又开始了大规模搜索。在快要放弃的时候看到了这篇文章:Android 弹性布局 FlexboxLayout了解一下
看到其中这张图的时候,两眼放光,异常激动啊。
2.jpg
看完是不是感觉希望就在前方? 于是看了下 flexboxLayout 的介绍,完完全全就是前端的 flexbox 布局啊,连属性名称都一样。因为有点 react native 基础,所以事情变得 so easy。最后看到竟然支持 RecycleView,只需改动极少量的代码,简直不能太赞。

嗯,废话那么多,铺垫那么长,下面来介绍一下今天的主角 : flexboxLayout 。

首先要做的事情当然是在项目中集成了:

1
compile 'com.google.android:flexbox:1.0.0'

1.什么是 flexboxLayout 布局?

https://github.com/google/flexbox-layout 项目简介中是这样一句话来概括 flexboxLayout 的:

FlexboxLayout is a library project which brings the similar capabilities of CSS Flexible Box Layout Module to Android.
FlexboxLayout 是一个在 Android 上实现 CSS 的 弹性盒状布局 模块的库

有前端基础的同学估计都知道 CSS 中这个布局,用来为盒状模型提供最大的灵活性。因为 android 中这个库属性和 CSS 中 都一样,并且阮一峰老师写的前端知识真的很通俗易懂,所以这里的介绍大多来自 Flex 布局教程。
3.png
采用 Flex 布局的元素,称为 Flex 容器(flex container),简称”容器”。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称”项目”。

容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。这里与 react native 相反,与前端 CSS 保持一致。

主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end

项目默认沿主轴排列。单个项目占据的主轴空间叫做 main size,占据的交叉轴空间叫做 cross size。

2.容器的属性 (FlexboxLayout 属性介绍)

这里说的容器也就是上面的采用了 Flex 布局的元素,在 android 中也就是引用了 FlexboxLayout 的 控件。即 FlexboxLayout 控件支持的属性。主要属性有:
4.png
各个属性的详细含义这里就不再赘述,阮一峰老师 这篇文章 写的超级棒,图文并茂,很容易理解,推荐大家看一下。

3.项目的属性 (子 View 属性介绍)

设置被 FlexboxLayout 包裹的子 View 的属性,因为 android 中的 flexboxLayout 布局 和 CSS 中 flex 布局关于子 View 属性有些差异,所以这里详细说明下,取值如下:

  • layout_order (integer)
  • layout_flexGrow (float)
  • layout_flexGrow (float)
  • layout_alignSelf
  • layout_flexBasisPercent (fraction)
  • layout_minWidth / layout_minHeight (dimension)
  • layout_maxWidth / layout_maxHeight (dimension)
  • layout_wrapBefore (boolean)

下面来看一下github文档中对这些属性的描述

3.1 layout_order

这个属性可以改变布局子视图的顺序。默认情况下,子元素的显示和布局顺序与布局XML中的顺序相同。如果没有指定,则将1设置为默认值( CSS 中默认值为 0) ,数值越小,排列越靠前。
5.gif
看下文档中的这张图,可以看到将 “2” 号 View 的 layout_order 属性设置为 -1时,由于其他的 View 默认都是 1,所以 “2” 号 view会排在最前面,同理,将 “2” 号 View 的 layout_order 的属性值设为 2 时,比其他默认值 1 都大,所以会排在最后。

3.2 layout_flexGrow

这个属性类似于 LinearLayout 中的 layout_weight 属性,如果没有指定,则将 0 设置为默认值。如果果同一 flex 行中的多个子 View 有正的 layout_flexGrow 值,那么剩余的空闲空间将根据它们声明的 layout_flexGrow 值的比例分布。

3.3 layout_flexShrink

该属性定义了子 View 的缩小比例,默认为 1,即如果空间不足,该子 View 将缩小。
如果所有子 View 的 layout_flexShrink 属性都为 1,当空间不足时,都将等比例缩小。如果一个项目的 layout_flexShrink 属性为0,其他子View都为 1,则空间不足时,layout_flexShrink 属性为 0 的不缩小。
6.gif
看一下文档中的这张图,开始设置所有子 view 的 layout_flexShrink 属性为1,添加子 view 的时候所有子 view 等比缩小,但是如果设置 layout_flexShrink 属性值为 0,子 view 将会按照原有比例显示,不缩小。

3.4 layout_alignSelf

layout_alignSelf 属性允许单个子 View 有与其他 View 不一样的对齐方式,可覆盖 align-items 属性。默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。
该属性可能取 6 个值,除了 auto,其他都与 align-items 属性完全一致

3.5 layout_flexBasisPercent

flex-layout_flexBasisPercent 属性定义了在分配多余空间之前,子 View 占据的主轴空间(main size)。根据这个属性来计算主轴是否有多余空间。它的默认值为 -1,即不设置,采用子 View 的本来大小。

如果设置了这个值,layout_width (或 layout_height )中指定的长度将被该属性的计算值覆盖。这个属性只有在父 View 的长度是确定的时候才有效(测量模式是 MeasureSpec.EXACTLY 模式下)。
并且该属性值只接受百分比值。
7.gif
可以分析下文档中的这张图:可以看到,如果把中间子 View 的这个属性值设为 50% 或 90%,那么这个 View 将占据主轴 50% 或 90% 的空间,然后剩余 View 会看有没有剩余空间换行。如果设置为 -1 默认值,那么将占据给定的大小。

3.6 layout_minWidth / layout_minHeight

这个属性设置了子 View 的最小的宽和高。在 layout_flexShrink 模式下,再怎么缩小也不会小于这个值

3.7 layout_maxWidth / layout_maxHeight

这个属性设置了子 View 的最大的宽和高。在 layout_flexGrow 模式下,再怎么放大也不会大于这个值

3.8 layout_wrapBefore

这个属性使得子 View 可以强制换行,不管在 main size 剩余空间有多少。这种对于类似 grid 网格布局中特殊设置某一个 item 布局特别有用。
这个属性是 CSS 中没有的属性。该属性在 flex_wrap 属性值 为 nowrap(不换行)的时候是无效的。
该属性结束 boolean 变量,默认 false,即不强制换行
9.gif
分析下文档中的这张图,“5” 号和 “6” 号 View 设置 layout_wrapBefore 属性为ture 的时候,不管前面剩余多少空间,都会强制换行

到这里,flexboxLayout 基本属性就介绍完毕了。

然后再来介绍一下跟 recycleView 结合使用。

4. 高能:与 RecyclewView 结合使用

Flexbox 能够作为一个 LayoutManager(FlexboxlayoutManager) 用在 RecyclerView 里面,这也就意味着你可以在一个有大量 Item 的可滚动容器里面使用 Flexbox,提高性能。具体使用示例:

1
2
3
4
5
6
7
8
9
10
//设置主轴方向为横轴
FlexboxLayoutManager manager = new FlexboxLayoutManager(this, FlexDirection.ROW);
//设置item沿主轴方向的位置
manager.setJustifyContent(JustifyContent.CENTER);
//设置item 沿次轴方向的位置
manager.setAlignItems(AlignItems.CENTER);

recycleView.setLayoutManager(manager);
centerGridAdapter = new CenterGridAdapter(items, this);
recycleView.setAdapter(centerGridAdapter);

可以看到跟 RecycleView 的其他 manager 使用一样,只需设置 manager 属性即可,属性值为上面叙述的几个容器的属性。
如果想对某个 item 进行单独的设置,可以在 adapter 中去设置,设置示例代码为:

1
2
3
4
5
6
ViewGroup.LayoutParams lp =  holder.itemLL.getLayoutParams();
if (lp instanceof FlexboxLayoutManager.LayoutParams) {
FlexboxLayoutManager.LayoutParams flexboxLp =
(FlexboxLayoutManager.LayoutParams) holder.itemLL.getLayoutParams();
flexboxLp.setFlexGrow(1.0f);
}

我这里是设置每个 item 有个权重(相当于 Linearlayout 的 weight 属性),所以会按比例分配 item 的宽,而不是我布局中设定的固定宽高。看下效果:
10.png

是不是有种键盘的感觉?并且我只是修改了极少的代码就实现了这个功能。

小试牛刀1

最后,看了那么多,回到最开始的问题上,现在知道类似微信的那个中间扩展的网格布局怎么写的吗?
首先我们简单分析一下,

  • 主轴方向我们应该设置为水平方向,即默认 flexDirection :“row”’
  • 可以换行,即 flexWrap:“wrap”
  • 子 View 在主轴方向的对其方式为居中(这一步实现从中间往两边展开),即 justifyContent: “center”
  • 子 View 在交叉轴方向的对其方式为居中,即 alignItems:”center”
  • 子 View 宽高固定

也就是上面讲 Recycleview 结合的例子中去掉单独设置 item 的部分,并且 item 的宽高要根据屏幕来适配的。
嗯,就是 so easy。

小试牛刀2

11.png
实际应用中还有种很常见的就是那种分类选择的布局,像图中的网易和简书中,这种布局用我们今天的这个主角是不是轻而易举的就实现了?都不用设置特别的属性,内容超过一行自动换行。
代码如下:

1
2
3
4
5
6
7
8
9
10
//设置主轴方向为横轴
FlexboxLayoutManager manager = new FlexboxLayoutManager(this, FlexDirection.ROW);
//设置item沿主轴方向的位置
manager.setJustifyContent(JustifyContent.FLEX_START);
//设置item 沿次轴方向的位置
manager.setAlignItems(AlignItems.CENTER);

recycleView.setLayoutManager(manager);
labelAdapter = new LabelAdapter(labels,this);
recycleView.setAdapter(labelAdapter);

总结

所以说了这么多,那么我们什么时候会用到这种布局呢?我目前想到的场景主要有 3 类:

  1. 类似LinearLayout 线性布局,但是又可以自动换行的
  2. 类似grid 网格布局的,但总有一两个item的排列方式很特立独行的
  3. 类似瀑布流的,但也是总有一两个item跟别人不一样的

当然这些场景加上 RecycleView 就会更加畅享丝滑了。

demo 传送门

参考

阮一峰:Flex 布局教程:语法篇
flexbox-layout 的 github 地址

android -- 善用 tools 属性让布局预览更加美好

发表于 2018-09-29 | 分类于 android | 阅读次数: ℃

前言

事情是这样的,前几周在做项目聊天页面 UI 微信一致化的时候,出现了如下场景:

QA:为什么个人信息页面刚进来的时候默认值是111?(额,抱歉,布局里写死了)
QA:你这个消息未读数位置太靠里了 ! (哦,赶紧找对应布局,发现默认是 gone 掉的, ok ,调成 visible ,调整位置完事 。)
QA: 你这个消息免打扰的图标太大了,图片颜色太重了,位置再往左边一点 !( 哦, 好的,马上调 )
QA: 群公告页面群名称太长的时候页面显示有问题,字体也太大了,字数卡控一下 ( 哦,好的,就调 )
QA: 你这个 。。。 ( 哦。。。)
经历了一番折腾与调整(基本改一两行代码运行跑一次,改 1 分钟,运行 3 分钟),再次提测,没一会,QA: 你怎么又改出问题了?为什么系统消息会显示免打扰图标? 免打扰的时候消息未读数展示也不对了,群消息页面的字展示还是有问题?( 哦,我查一下代码 )

最后查了代码发现之前默认隐藏的布局,我为了调样式设置为 visible 后忘了调回去了,然后代码里也没做 gone 的判断。
那类似这样的调整布局的方式有没有什么奇淫技巧,让我不改变我之前设定的值还能预览到我改变后的变化的?
答案就是 :利用 tools 属性。

认识 tools 属性

在android studio 3.0 以后,会发现每次创建布局的时候再根<View> 都会自动添上 xmlns:tools="http://schemas.android.com/tools" 命名空间 和 tools:context 标签。
如果我们删除这个属性,好像布局也不会有什么变化,那它到底有什么用呢?首先看一下文档上的解释:

tools 命名空间在 Android Studio 中支持各种 XML 属性,这些属性支持设计时特性(如在片段中显示的布局)或编译时行为(如应用于 XML 资源的 shrinking 模式)。在构建应用程序时,构建工具会删除这些属性,因此不会影响 APK 大小或运行时行为。

总结起来也就是以下两点:

  • tools 属性作用分为两类 : 设计时特性(主要表现在 XML 布局预览) 和 编译时行为
  • 在 build 程序的时候,这些属性不会被编译,对APK 没有影响,也就是放心大胆地用吧

所以,我们下面来具体看一下,第一类:设计时特性(XML 布局预览效果)

Design-time view attributes (设计时特性)

首先需要注意的是这些属性只在 android studio 的布局预览页面有效

1. tools: (instead of android)

用 tools 命名空间可以替代所有以 android 命名空间开头的属性。比如tools:visibility 。
使用对象:任何<View>
使用场景:比如预览某个 View 显示与隐藏,TextView 字体大小,多行显示,给某个 View 设置背景色等等,可以替代任何一个 android 命名空间的属性值
tool1.png

2.tools:context

这个属性声明了该布局关联的 Activity。使编辑器或布局预览中需要了解 Activity 的特性成为可能。
使用对象:任何根<View>
使用场景:(1)设置预览主题为关联 Activity 的主题 (2)View 的 onclick 方法快速自动插入到关联 Activity 中

tools2.png
tools3.png
对于在布局中写 onClick 的是不是爽到爆?

3. tools:layout

这个属性是展示 fragment 对应的布局。
使用对象:<fragment>
使用场景:在 XML 中直接引用fragment 标签的。
如果我们不引用这个属性,布局预览中会这样显示:
tools4.png
是看不到对应的 fragment 的布局的,当我们引用了tools:layout属性后,就可以预览 fragment 对应的布局
tools5.png

4.tools:listitem / tools:listheader / tools:listfooter

这一组特性专为<AdapterView>比如 ListView 和 RecycleView 打造。见名知意,其中:

  • tools:listitem是设置 item 布局的预览
  • tools:listheader设置添加 ListView/RecycleView 的头布局预览
  • tools:listfooter设置添加ListView/RecycleView 的 footer 布局预览

实际操作中发现在 RecycleView 中 header 和 footer 显示不出来
tools10.png

注意事项:Listview/RecycleView 一定要声明 id 才会有效,footer 效果一般显示不出来

5.tools:itemCount

这个属性指定 Recycleview 显示多少行
使用对象:<RecycleView>
使用场景:比如我们布局中 RecycleView 下面还有内容或者想看只有一条数据的时候的效果,都可以指定可预览的行数,比如上面的 RecycleView 指定只显示三行,显示如下图:
tools11.png

5. tools:showin

这个属性作用在由 include 标签引用的根 <View> 上,可以预览当前 include 布局在其引用的父类 View 布局中的效果,方便预览和编辑当前布局。
使用对象:任何 include 标签指向的布局的 根 View
使用场景:比如我有一个布局 activity_include, 里面布局如下:

tools6.png

假设我的 include 里面的布局很复杂,然后我现在要去修改 include 里面的布局,点进入,先预览一下之前的页面:
tools7.png
然后,我经过了一番大的改动,想看看这个布局在引用的父布局 activity_include 中的效果,但是呢我又不想再切换到 activity_include 布局中去检查,万一改的不对,岂不是还要再切换回来,于是我加上tools:showIn="@layout/activity_include" ,然后在当前预览页面就可以看到完整的页面了:
tools8.png

是不是很方便?
除了这几个属性,还有tools:menu、tools:minValue / tools:maxValue、tools:openDrawer、"@tools:sample/*" resources 这几个,由于不(wo)常(bi)用(jiao)到(lan),这里就不一一介绍了,大家可以查阅文档

最开始的时候我们说过,tools 属性主要包含两类,一类设计时特性,用于页面预览,另一类就是编译时行为了

Error handling attributes(编译时属性)

这类属性主要是有助于抑制 Lint 警告消息。

1.tools:ignore

主要用于设置 Lint 忽略一些信息。 比如,我们 string 字符串有些进行了多语言的翻译,但是有些由于时间匆忙,来不及翻译,运行的时候编译器又会报警告,这个时候我们就可以用 tools:ignore 来让 Lint 忽略这些

2.tools:targetApi

这个跟上面的一样,同样是让 Lint 忽略一些信息的,比如有些控件只在 targetApi = 11 的时候生效,那我们现在的 targetApi = 25,就会报 minSdkVersion 的警告,那么我们就通过 tools:targetApi 设置目标 api

3.tools:locale

这个属性是用来告诉编译工具在给定资源的元素中默认的语言/区域设置是什么(因为工具默认采用英语),以避免拼写检查器发出警告。该值必须是有效的地区限定符。
使用对象:<resources>
例如,我们可以将其添加到我们的字符串中告诉编译器我们默认使用中文而不是英语
tools9.png

总结

大致就这么多,主要应用在布局预览中。
tools 属性一般会被忽略,但是在调试布局的时候真的是把利器。所以善用它,我们可以更高效的预览和修改布局,有句话说的好,所见即所得。
最后附上一张表格:

属性 作用对象 描述
tools: (替代android:) <View>对象 可替代所有以android:开头的属性
tools:context 根<View> 声明了该布局关联的Activity
tools:layout <fragment> 展示 fragment 对应的布局
tools:listitem <List>/<RecycleView> 展示 ListView/RecycleView 的item布局
tools:listheader <List>/<RecycleView> 展示 ListView/RecycleView 的header布局
tools:listfooter <List>/<RecycleView> 展示 ListView/RecycleView 的footer布局
tools:listfooter <List>/<RecycleView> 展示 ListView/RecycleView 的footer布局
tools:itemCount <RecycleView> 指定 RecycleView 展示多少行
tools:showin <include>所指向布局的根<View> 预览当前include布局在其引用的父类View布局中的效果,方便预览和编辑当前布局
tools:ignore Lint 让编译器忽略警告
tools:targetApi Lint 指定targetApi,让编译器忽略targetApi问题警告
tools:locale Lint, Android Studio editor 设置给定资源的元素中默认的语言,以避免拼写检查器发出警告

参考文章

https://developer.android.com/studio/write/tool-attributes#error_handling_attributes

12
xiaxiayang

xiaxiayang

万事起于乎微

14 日志
4 分类
© 2019 xiaxiayang