与ViewPager完美结合的顺滑引导条系列之一

引言


与ViewPager完美结合的顺滑引导条系列之需求分析这篇文章里已经了解了基本的需求,分析了一些交互行为。布局方面,我首先想到的是用RecyclerView完成索引,目的是让索引项可以复用减少内存开销,然后画一个View做光标。

滑动逻辑,是通过ViewPager.OnPageChangeListener中onPageScrolled回调的第二个参数positionOffset获得ViewPager滑动的比例,可以通过此参数计算光标和索引需要滑动的距离

正文


为了保证通用性将引导条封装成一个单独的控件GuideRecyclerView,布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="44dp">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="44dp" />

    <View
        android:id="@+id/lineView"
        android:layout_width="10dp"
        android:layout_height="3dp"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="7dp"
        android:background="#444444" />
</RelativeLayout>

使用时只需要三行代码

        guideRecyclerView = findViewById(R.id.guiderecyclerview);
        guideRecyclerView.setAdapter(recyclerViewAdapter);
        viewPager.addOnPageChangeListener(guideRecyclerView.getOnPageChangeListener());

这里的Adapter是RecyclerView的,数据也采用最简单的List<String>。OnPageChangeListener则是在GuideRecyclerView内部实现方便复用。

GuideRecyclerView

重点代码分析

    public ViewPager.OnPageChangeListener getOnPageChangeListener() {
        return new ViewPager.SimpleOnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                Log.i("onPageScrolled", "position-->" + position + "     positionOffset-->" + positionOffset + "     positionOffsetPixels-->" + positionOffsetPixels);
                setScroll(position, positionOffset);
            }
        };
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (lineViewMarginLeft == 0) {//第一次布局游标
            layoutLineView(0);
        }
    }

    private void setScroll(int position, float offset) {

        Log.i("setScroll", "position-->" + position + "     offset-->" + offset);

        if (!whetherOrNotToMove(position, offset)) {
            return;
        }

        if (!positiveDirection) {//根据方向设置偏移
            offset = offset - 1;
        }

        int x = calculateMoveLength(position, offset);

        move(x);
    }

    private boolean whetherOrNotToMove(int position, float offset) {
        if (position > currentPosition) {//第一次正向滑动触发
            resetState(position);
            layoutLineView(currentPosition);
            positiveDirection = true;
            return false;
        }
        if (position < currentPosition) {//第一次反向滑动触发
            resetState(position);
            layoutLineView(currentPosition + 1);
            positiveDirection = false;
            return false;
        }

        if (position == 0 && offset == 0) {//滑到第一个 初始状态
            resetState(position);
            layoutLineView(0);
            positiveDirection = true;
            return false;
        }
        return true;
    }

    private void resetState(int position) {//重置状态
        currentPosition = position;
        whichOne = 0;
        lastx = 0;
    }

    private void layoutLineView(int currentPosition) {//将游标布局到指定位置
        View item = recyclerView.getLayoutManager().findViewByPosition(currentPosition);
        lineViewMarginLeft = item.getLeft() + (item.getWidth() - lineView.getWidth()) / 2;

        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) lineView.getLayoutParams();
        params.leftMargin = lineViewMarginLeft;
        lineView.setLayoutParams(params);
    }

    private int calculateMoveLength(int position, float offset) {
//        recyclerView.getChildCount() 只有当前显示的数目
//        recyclerView.getChildAt() 只能获取当前显示的view
        int temp = position;
        if (temp >= recyclerView.getAdapter().getItemCount()) {
            temp = recyclerView.getAdapter().getItemCount() - 1;
        }
        View currentView = recyclerView.getLayoutManager().findViewByPosition(temp);//当前所指view

        int lastPosition = position + 1;
        if (lastPosition >= recyclerView.getAdapter().getItemCount()) {
            lastPosition = recyclerView.getAdapter().getItemCount() - 1;
        }
        View nextView = recyclerView.getLayoutManager().findViewByPosition(lastPosition);//将要滑动到的view

        Log.i("viewnull", temp + "   currentView-->" + currentView + "----------" + lastPosition + "     nextView-->" + nextView);

        int width = (currentView.getWidth() + nextView.getWidth()) / 2;//计算两view之间的总的滑动距离
        return Math.round(width * offset);//此次滑动的距离
    }

    private void move(int x) {
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) lineView.getLayoutParams();
        if (whichOne == 0) {//判断   一次完整的滑动   是否结束      一次滑动结束后 whichOne=0
            if (positiveDirection) {
                if (params.leftMargin > 540 && recyclerView.canScrollHorizontally(1)) {//游标在右半边且recyclerView可以右滑 则滑动recyclerView
                    whichOne = whichOne | 1;
                } else {
                    whichOne = whichOne | 2;
                }
            } else {
                if (params.leftMargin < 540 && recyclerView.canScrollHorizontally(-1)) {//游标在左半边且recyclerView可以左滑 则滑动recyclerView
                    whichOne = whichOne | 1;
                } else {
                    whichOne = whichOne | 2;
                }
            }
        }

        if (whichOne == 1) {
            recyclerView.scrollBy(x - lastx, 0);//因为scrollBy的滑动是累加的,所以必须减去上次滑动的距离,实际移动的距离为差值  exp:第一次滑动1 第二次滑动2,第二次滑动3,scrollBy会滑动6,但实际我只想滑动3
            lastx = x;
        } else {
            params.leftMargin = lineViewMarginLeft + x;
            lineView.setLayoutParams(params);
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {//viewpager还在滑动的时候recyclerView不许滑动    算了直接不让滑动否则会造成空指针
        return true;
    }

初始化

控件首次显示时会调用onLayout和onPageScrolled

最初显示时游标并不知道自己的位置所以要在onLayout中调用layoutLineView(0)一次,让游标初始化起始位置

onPageScrolled是ViewPager初次显示时的回调,此时默认参数全部为0

滑动

ViewPager正向滑动时position不变positionOffset逐渐变大
ViewPager反向滑动时position即刻减一positionOffset逐渐减小
positionOffset范围:[0,1)

从代码可以看到当ViewPager滑动的时候会执行setScroll方法,其内部主要逻辑如下:

whetherOrNotToMove:返回是否需要滑动,内部做了如下处理:重置状态、布局游标、判断滑动方向

resetState:当一次完整的滑动结束后状态重置

layoutLineView:当一次完整的滑动结束后摆放游标位置

由于反向滑动positionOffset是逐渐减小的,于是需要转

        if (!positiveDirection) {//根据方向设置偏移
            offset = offset - 1;
        }

calculateMoveLength:计算滑动的距离

move:移动

  • whetherOrNotToMove 判断是否滑动
    三个if代表了三种不同情况下滑动状态的终结
    情景分析:
    如果最初在0位置,手势左滑,此时currentPosition=0,参数position=0,offset逐渐增加,此时返回true,表示滑动开始,只有在position== currentPosition时才会滑动
    第一个if:当滑动到1位置时,此时参数position=1,offset可忽略(因为不固定,并不一定返回0),这时是第1项选中的状态,完成了一次滑动,因此需要重置状态将游标摆在第1项下,记录滑动的方向,并返回false,表示一次滑动结束
    第二个if:反向滑动时position立即减1,与当前currentPosition不同,做滑动前的状态重置,将游标放在当前的项下而不是目标项下记录滑动的方向,并返回false
    第三个if:这时为了提防onPageScrolled首次回调使用,与onLayout同理

  • resetState重置状态
    将传入的position赋值当前位置currentPosition。
    将上一次滑动对象是谁的状态清除。
    lastx代码中有注释需要注意

  • layoutLineView将游标摆放特定位置
    获取显示项View距父控件的x坐标,并将游标居中于显示项,算出距离设置Margin

  • calculateMoveLength计算移动距离
    得到当前项和目标项View,计算两View中心点间的距离乘以偏移量就得出了移动距离。
    这里注意RecyclerView中getChildCount和getChildAt两个方法,它是获取当前显示的ChildCount和当前显示的ChildAt,注意使用

  • move移动
    这里可移动的有光标和索引因此需要记录此次滑动的是谁,如果上次滑动没有结束继续上次滑动。

最终效果

引导条可滑动
重写了dispatchTouchEvent

缺陷

  • 索引滑动时游标不动
  • 一次完整的滑动必须是索引或游标,不能共存
  • 频繁不规则地滑动ViewPager或引导条时,会引起NullPointerException,因为RecyclerView找不到内部的View(所以才重写了dispatchTouchEvent)

为了解决上述问题见与ViewPager完美结合的顺滑引导条系列之二

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

推荐阅读更多精彩内容