【译】RecyclerView+ItemDecorations实现带指示器ViewPager效果

原文地址
https://blog.davidmedenjak.com/android/2017/06/24/viewpager-recyclerview.html
先贴最后的效果图,所有完整的代码可以在这里找到,觉得不错的可以给个赞

viewpager.gif

我们知道如何通过ViewPager来展示多页面,但从 support library24.2.0推出后,你可以通过SnapHelper这个类轻松给RecyclerView加上类似ViewPager的效果,这篇文章是告诉你如何给你的RecyclerView添加page indicators,如果你有阅读过我的博客的话,你可能知道我接下来会怎么做:

Pager 初始化

第一步,初始化你的RecyclerView,确保你的itemlayout设置成了layout_width="match_parent",否则的话没那么容易一页一页滚动。你的RecyclerView高度也应该设置成match_parent,如果是设置成wrap_content的话要确保你的items也要有相同的高度。

PagerSnapHelper添加到你的RecyclerView


// 给recyclerview添加background color

recyclerView.setBackgroundColor(backgroundColor);

MyAdapter adapter = ...

recyclerView.setAdapter(adapter);

recyclerView.setLayoutManager(new LinearLayoutManager(context,

LinearLayoutManager.HORIZONTAL, false));

// 添加PagerSnapHelper

PagerSnapHelper snapHelper = new PagerSnapHelper();

snapHelper.attachToRecyclerView(recyclerView);

我们现在有了个简单的可以翻页滚动的RecyclerView,当我们添加一个background color 在这里的话,在底部画白色的decorations就会比较好看。

bland-pager.gif

添加Pager Indicator

提示:如果你对于decorations没有任何了解的话你可以通过 introduction to decorations 入门我是如何简单在items之间画一个下划线Indicator

下一步我们需要添加decoration来画indicator,我们创建一个LinePagerIndicatorDecoration并把它添加到RecyclerView


// pager indicator

recyclerView.addItemDecoration(newLinePagerIndicatorDecoration());

我们的decoration要关注2个方法

getItemOffsets:给每个ItemView添加padding,不会出现overlaying

onDrawOver:在上层画decoration ,绘制的内容在itemview上层。

我喜欢用getItemOffsets方法来确保我的items没有overdraw,如果你的indicator 倾向overdraw,你可以忽略这个getItemOffsets方法。我们做
的一切是需要indicatorHeight的偏移在每个View的底部。如果你使用
GridLayoutManager,你需要确保你的items仅仅只是在底部偏移了


@Override

public void getItemOffsets(Rect outRect, View view,

RecyclerView parent, RecyclerView.State state) {

super.getItemOffsets(outRect, view, parent, state);

outRect.bottom = indicatorHeight;

}

这个在底部的偏移也是我为什么设置了一个RecyclerViewbackground而不是pages上面, 这个偏移让我们的decorationcontent留有一点距离,所以设置一个background colorpagesitem上面的话会没有效果。如果你不偏移你的itemsoverlay他们的话,你也就不需要给RecyclerView 设置background

接下来我们把indicators画给我们的pages。我们把indicator置于RecyclerView的底部中间,并且给每个item画直线,每个直线之间有一些padding


@Override

public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {

super.onDrawOver(c, parent, state);

int itemCount = parent.getAdapter().getItemCount();

//水平居中, 计算宽度减去距离中间的一半

float totalLength = mIndicatorItemLength * itemCount;

float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;

float indicatorTotalWidth = totalLength + paddingBetweenItems;

float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;

// 在剩下的space垂直居中

float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;

drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);

}

private void drawInactiveIndicators(Canvas c, float indicatorStartX,

float indicatorPosY, int itemCount) {

mPaint.setColor(colorInactive);

// item indicator的宽度包含padding

final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

float start = indicatorStartX;

for (int i = 0; i < itemCount; i++) {

// 给每个item画下划线

c.drawLine(start, indicatorPosY,

start + mIndicatorItemLength, indicatorPosY, mPaint);

start += itemWidth;

}

}

这个地方给了我们机会给每个item画一个标记,但是这些标记在page是选中后的时候还不是高亮的。接下来我们计算我们滚动了多远来实现一个水平滚动的动画并且把高亮的indicator画出来。

我们通过LayoutManager找出当前活动的page,然后计算滑动距离的百分比。这个计算方法在你的Views宽度设置成了match_parent的时候会很有效简单,否则的话可能会有不确定的情况。为了改善体验我使用了AccelerateDecelerateInterpolator来处理这个得到的百分比progress的值,让它看起来更加自然。


//找到活动的page,它这时候的下划线应该是高亮的

LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();

int activePosition = layoutManager.findFirstVisibleItemPosition();

if (activePosition == RecyclerView.NO_POSITION) {

return;

}

// 找到活动的page偏移的距离 (如果用户滑动了)

final View activeChild = layoutManager.findViewByPosition(activePosition);

int left = activeChild.getLeft();

int width = activeChild.getWidth();

// 滑动时候活动的item位置在[-width, 0]

// 滑动时候加入平滑动画

float progress = mInterpolator.getInterpolation(left * -1 / (float) width);

通过这个百分比progress我们就可以画这个高亮的indicator,它代表着用户滚动到了哪个page.我们使用这个百分比progress来画这个局部高亮选中的indicator它代表我们滚动到了哪个page


public void onDrawOver(Canvas c, RecyclerView parent,

RecyclerView.State state) {

super.onDrawOver(c, parent, state);

// 画正常状态下的下划线

// ...计算百分比 ...

// 画高亮状态下的下划线

drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress, itemCount);

}

private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,

int highlightPosition, float progress, int itemCount) {

mPaint.setColor(colorActive);

// 每个item的下划线是包括padding

final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

if (progress == 0F) {

//百分比为0没有滑动的话第一个画高亮下划线

float highlightStart = indicatorStartX + itemWidth * highlightPosition;

c.drawLine(highlightStart, indicatorPosY,

highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);

} else {

float highlightStart = indicatorStartX + itemWidth * highlightPosition;

// 计算局部高亮下划线的长度

float partialLength = mIndicatorItemLength * progress;

// 画断开的下划线

c.drawLine(highlightStart + partialLength, indicatorPosY,

highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);

// 画高亮的下划线 覆盖在下一个item 

if (highlightPosition < itemCount - 1) {

highlightStart += itemWidth;

c.drawLine(highlightStart, indicatorPosY,

highlightStart + partialLength, indicatorPosY, mPaint);

}}

}

通过上面所有步骤,我们达到了预期的indicator指示器,在RecyclerView正确的实现page效果

viewpagerdecoration.gif

所有完整的代码可以在这里找到

还有什么要做的?

你可能发现了,我选择线条来代替圆圈做indicator指示器,但是画圆圈通过动画来设置他们的alpha也是可以轻松实现类似的效果的。通过使用类似的方法你可以在decorations 做很多事,你不需要修改代码就可以拿来重复使用。

这个解决方案只是一个尝试,可能还有一些潜在的错误。正如文中提到的,确定这个progress的方法在不同宽度的时候可能就不准确,更好的方法可能是需要在SnapHelper内部去做处理。如果你选择使用这个在你的APP的话要确保有足够的测试。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 简介: 提供一个让有限的窗口变成一个大数据集的灵活视图。 术语表: Adapter:RecyclerView的子类...
    酷泡泡阅读 5,113评论 0 16
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,049评论 25 707
  • 前言 最近在做项目的时候,有个需求就是实现自动轮播式的ViewPager,最直观的例子就是知乎日报顶部的ViewP...
    丶蓝天白云梦阅读 8,150评论 13 77
  • 每天都是新的开始,新的起点,你想要比别人强,你就必须去付出比别人更多的努力,你想要更好的生活,那么你就必须去承受更...
    Lzr_2017阅读 242评论 0 2
  • 你跟朋友一起在豪华商场里瞎逛,在一家店里试了两套衣服,准备买时看到标签上亮瞎人眼睛的一串数字,无奈将衣服放下,夺门...
    李小说阅读 335评论 1 1