1. 1. 应对复杂结构列表
  2. 2. 列表滚动速度控制
  3. 3. 列表项的自我位置感知
  4. 4. 填埋官方遗留之坑

手机屏幕尺寸有限,几乎每个 app 的内容区,都会用列表呈现。不信可以打开你手机里的任意一个 app 看看,99%的 app 的内容区都能被抽象成一个大列表。只是根据内容的丰富度,列表的复杂度有所不同。内容区的结构骨架,基本上就是由列表布局奠定的。从这点看,虽然还有其他各种类型的布局,但是它们都没有列表型的布局来得重要。
总的来说,必须知道的有 如何应对多类型复杂结构几个滚动相关的实用的特别交互、和使用 RecyclerView 注意要填的坑。这些足以覆盖 90% 的业务场景。
(Android 的列表布局多由RecyclerView实现,以下内容均基于RecyclerView)

应对复杂结构列表

我安利 MultiType

从前,比如我们写一个类似微博列表页面,这样的列表是十分复杂的:有纯文本的、带转发原文的、带图片的、带视频的、带文章的等等,甚至穿插一条可以横向滑动的好友推荐条目。不同的 item 类型众多,而且随着业务发展,还会更多。如果我们使用传统的开发方式,经常要做一些繁琐的工作,代码可能都堆积在一个 Adapter 中:我们需要覆写 RecyclerView.Adapter 的 getItemViewType 方法,罗列一些 type 整型常量,并且 ViewHolder 转型、绑定数据也比较麻烦。一旦产品需求有变,或者产品设计说需要增加一种新的 item 类型,我们需要去代码堆里找到原来的逻辑去修改,或找到正确的位置去增加代码。这些过程都比较繁琐,侵入较强,需要小心翼翼,以免改错影响到其他地方。
现在好了,我们有了 MultiType,简单来说,MultiType 就是一个多类型列表视图的中间分发框架,它能帮助你快速并且清晰地开发一些复杂的列表页面,数据驱动视图。 它本是为聊天页面开发的,聊天页面的消息类型也是有大量不同种类,且新增频繁,而 MultiType 能够轻松胜任。

列表滚动速度控制

通过自定义 LayoutManager 的 SmoothScroller。

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
public class WrapContentLinearLayoutManager extends LinearLayoutManager {
private static float MILLISECONDS_PER_INCH = 25f; //default is 25f (bigger = slower)
public WrapContentLinearLayoutManager(Context context) {
super(context);
}

public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}

public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {

final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {

@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return WrapContentLinearLayoutManager.this.computeScrollVectorForPosition(targetPosition);
}

@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};

linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}

@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
if (state == RecyclerView.SCROLL_STATE_IDLE) { // on scroll stop
setSpeedSlow(25f);
}
}

public void setSpeedSlow(float msPerInch) {
MILLISECONDS_PER_INCH = msPerInch;
}
}

列表项的自我位置感知

用来应对当 XXX 可见/滑到屏幕中间/正在滑进屏幕/正在滑出屏幕 就要干 XXX 这类奇怪需求。

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
/**
* 获得给定view的可见度,0~100
*/

public int getVisibilityPercents(View view) {
if(SHOW_LOGS) Logger.v(TAG, ">> getVisibilityPercents view " + view);

int percents = 100;
Rect mCurrentViewRect = new Rect();;
view.getLocalVisibleRect(mCurrentViewRect);
if(SHOW_LOGS) Logger.v(TAG, "getVisibilityPercents mCurrentViewRect top " + mCurrentViewRect.top + ", left " + mCurrentViewRect.left + ", bottom " + mCurrentViewRect.bottom + ", right " + mCurrentViewRect.right);

int height = view.getHeight();
if(SHOW_LOGS) Logger.v(TAG, "getVisibilityPercents height " + height);

if(viewIsPartiallyHiddenTop(mCurrentViewRect)){
// view is partially hidden behind the top edge
percents = (height - mCurrentViewRect.top) * 100 / height;
} else if(viewIsPartiallyHiddenBottom(mCurrentViewRect,height)){
percents = mCurrentViewRect.bottom * 100 / height;
}

if(SHOW_LOGS) Logger.v(TAG, "<< getVisibilityPercents, percents " + percents);

return percents;
}
private boolean viewIsPartiallyHiddenBottom(Rect mCurrentViewRect ,int height) {
return mCurrentViewRect.bottom > 0 && mCurrentViewRect.bottom < height;
}

private boolean viewIsPartiallyHiddenTop(Rect mCurrentViewRect ) {
return mCurrentViewRect.top > 0;
}

填埋官方遗留之坑

规避 RecyclerView 的一个不会报到Java层的bug(直接崩溃,不会有弹窗提示崩溃),因为在底层直接抛了IndexOutOfBoundsException。可以关注AOSP关于该Bug的记录,跟踪是否有fix:https://issuetracker.google.com/issues/37007605#hc141

暂时只能在 LinearLayoutManager 类对应的方法中 catch 掉等官方解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
}

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
return super.scrollVerticallyBy(dy, recycler, state);
} catch (IndexOutOfBoundsException e) {
e.printStackTrace();
}
return 0;
}