RecyclerView.ItemDecoration实现指示器效果

参考:https://github.com/bleeding182/recyclerviewItemDecorations/blob/master/app/src/main/java/com/github/bleeding182/recyclerviewdecorations/viewpager/LinePagerIndicatorDecoration.java

该效果是一个长线型的指示器,在此基础上修改定制成圆形指示器。


Screenrecorder-2020-03-16-13-50-29-163.gif
package com.gujingli.recycler.util;

import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;

public class DropIndicator extends RecyclerView.ItemDecoration {

    private int mIndicatorHeight;//指示器所占高度
    private int indicatorCount;
    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();//插值器:先加速再减速
    private static final float DP = Resources.getSystem().getDisplayMetrics().density;//屏幕密度
    private Paint indicatorPaint;//指示器画笔
    private Paint selectorPaint;//选中器画笔
    private Path mPath = new Path();//选中器的形状
    private float radius = DP * 3;//圆直径
    private final double c = 0.552284749831;//path画圆固定值
    private float startX;//记录选中器的中心点X坐标
    private float startY;//记录指示器及选中器的中心点Y坐标
    private float firstX;//记录指示器及选中器的起始中心点X坐标
    private XPoint p2, p4;//右左切线
    private YPoint p1, p3;//下上切线
    private float mc;//切线半长
    private float div = DP * 12;//指示器间距
    private int currentPos = 0;//当前
    private int toPos = -1;
    private boolean direction = true;//方向
    private float distance;//位移
    private float mCurrentTime;//当前状态
    private float lastCurrentTime = 0;//最后状态
    private static final String TAG = "DropIndicator";//日志

    public DropIndicator() {
        //初始化
        mc = (float) (c * radius);
        mIndicatorHeight = (int) (radius * 2 + div);
        //未选中
        indicatorPaint = new Paint();
        indicatorPaint.setColor(0xFFE5E5E5);
        indicatorPaint.setStyle(Paint.Style.FILL);
        indicatorPaint.setAntiAlias(true);
        indicatorPaint.setStrokeWidth(1);
        //选中
        selectorPaint = new Paint();
        selectorPaint.setColor(0xFF000000);
        selectorPaint.setStyle(Paint.Style.FILL);
        selectorPaint.setStrokeWidth(1);
        selectorPaint.setAntiAlias(true);
        //初始化选中点
        p1 = new YPoint(0, radius, mc);//下
        p3 = new YPoint(0, -radius, mc);//上
        p2 = new XPoint(radius, 0, mc);//右
        p4 = new XPoint(-radius, 0, mc);//左
    }

    //初始化选中点
    private void resetP() {
        p1.setY(radius);
        p1.setX(0);
        p1.setMc(mc);

        p3.setY(-radius);
        p3.setX(0);
        p3.setMc(mc);

        p2.setY(0);
        p2.setX(radius);
        p2.setMc(mc);

        p4.setY(0);
        p4.setX(-radius);
        p4.setMc(mc);
    }

    //绘制选中点
    protected void dispatchDraw(Canvas canvas) {
        mPath.reset();
        if (mCurrentTime == 0) {//圆
            resetP();
            canvas.translate(startX, startY);//设置圆点位置
            if (toPos > currentPos) {
                p2.setX(radius);
            } else {
                p4.setX(-radius);
            }
        }
        if (mCurrentTime > 0 && mCurrentTime <= 0.2) {//第一阶段,前端变尖
            direction = toPos > currentPos ? true : false;
            canvas.translate(startX, startY);//设置圆点位置
            if (toPos > currentPos) {
                p2.setX(radius + 2 * 5 * mCurrentTime * radius / 2);//改变p2.x坐标
            } else {
                p4.setX(-radius - 2 * 5 * mCurrentTime * radius / 2);//改变p4.x坐标
            }
        } else if (mCurrentTime > 0.2 && mCurrentTime <= 0.5) {//第二阶段,开始移动变形
            canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);//设置圆点位置
            if (toPos > currentPos) {
                p2.setX(2 * radius);//p2.x坐标达到最大值
                p1.setX(0.5f * radius * (mCurrentTime - 0.2f) / 0.3f);//p1.x坐标移动
                p3.setX(0.5f * radius * (mCurrentTime - 0.2f) / 0.3f);//p3.x坐标移动
                p2.setMc(mc + (mCurrentTime - 0.2f) * mc / 4 / 0.3f);//p2.mc间距变大
                p4.setMc(mc + (mCurrentTime - 0.2f) * mc / 4 / 0.3f);//p4.mc间距变大
            } else {
                p4.setX(-2 * radius);//p4.x坐标达到最大值
                p1.setX(-0.5f * radius * (mCurrentTime - 0.2f) / 0.3f);//p1.x坐标移动
                p3.setX(-0.5f * radius * (mCurrentTime - 0.2f) / 0.3f);//p3.x坐标移动
                p2.setMc(mc + (mCurrentTime - 0.2f) * mc / 4 / 0.3f);//p2.mc间距变大
                p4.setMc(mc + (mCurrentTime - 0.2f) * mc / 4 / 0.3f);//p4.mc间距变大
            }
        } else if (mCurrentTime > 0.5 && mCurrentTime <= 0.8) {//第二阶段,继续移动变形,逐渐恢复
            canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);//设置圆点位置
            if (toPos > currentPos) {
                p1.setX(0.5f * radius + 0.5f * radius * (mCurrentTime - 0.5f) / 0.3f);
                p3.setX(0.5f * radius + 0.5f * radius * (mCurrentTime - 0.5f) / 0.3f);
                p2.setMc(mc + (mCurrentTime - 0.5f) * mc / 4 / 0.3f);//p2.mc间距变大
                p4.setMc(mc + (mCurrentTime - 0.5f) * mc / 4 / 0.3f);//p4.mc间距变大
            } else {
                p1.setX(-0.5f * radius - 0.5f * radius * (mCurrentTime - 0.5f) / 0.3f);
                p3.setX(-0.5f * radius - 0.5f * radius * (mCurrentTime - 0.5f) / 0.3f);
                p2.setMc(mc + (mCurrentTime - 0.5f) * mc / 4 / 0.3f);//p2.mc间距变大
                p4.setMc(mc + (mCurrentTime - 0.5f) * mc / 4 / 0.3f);//p4.mc间距变大
            }
        } else if (mCurrentTime > 0.8 && mCurrentTime <= 0.9) {
            p2.setMc(mc);//mc回复呈圆形
            p4.setMc(mc);//mc回复呈圆形
            canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);//设置圆点位置
            if (toPos > currentPos) {
                p4.setX(-radius + 1.6f * radius * (mCurrentTime - 0.8f) / 0.1f);
            } else {
                p2.setX(radius - 1.6f * radius * (mCurrentTime - 0.8f) / 0.1f);
            }
        } else if (mCurrentTime > 0.9 && mCurrentTime < 1) {
            canvas.translate(startX + distance, startY);//设置圆点位置
            if (toPos > currentPos) {
                p1.setX(radius);
                p3.setX(radius);
                p4.setX(0.6f * radius - 0.6f * radius * (mCurrentTime - 0.9f) / 0.1f);
            } else {
                p1.setX(-radius);
                p3.setX(-radius);
                p2.setX(-0.6f * radius + 0.6f * radius * (mCurrentTime - 0.9f) / 0.1f);
            }
        }
        //结束回复圆形状态
        if (mCurrentTime == 1) {
            lastCurrentTime = 0;
            canvas.translate(startX + distance, startY);
            if (direction) {
                p1.setX(radius);
                p3.setX(radius);
                p4.setX(0);
            } else {
                p1.setX(-radius);
                p3.setX(-radius);
                p2.setX(0);
            }
            currentPos = toPos;
            resetP();
            if (direction)
                canvas.translate(radius, 0);
            else
                canvas.translate(-radius, 0);
        }
        mPath.moveTo(p1.x, p1.y);//起始位置下中点
        mPath.cubicTo(p1.right.x, p1.right.y, p2.bottom.x, p2.bottom.y, p2.x, p2.y);//右下弧
        mPath.cubicTo(p2.top.x, p2.top.y, p3.right.x, p3.right.y, p3.x, p3.y);//右上弧
        mPath.cubicTo(p3.left.x, p3.left.y, p4.top.x, p4.top.y, p4.x, p4.y);//左上弧
        mPath.cubicTo(p4.bottom.x, p4.bottom.y, p1.left.x, p1.left.y, p1.x, p1.y);//左下弧
        canvas.drawPath(mPath, selectorPaint);//绘制选中器
    }


    //更新选中器状态
    private void updateDrop(Canvas c, int position, float positionOffset) {
        //判断滑动方向,确定移动位置toPos
        if ((position + positionOffset) - currentPos > 0)//正
            direction = true;
        else if ((position + positionOffset) - currentPos < 0)//反
            direction = false;
        if (direction)
            toPos = currentPos + 1;
        else
            toPos = currentPos - 1;
        //更新圆点
        startX = firstX + radius + (currentPos) * (div + 2 * radius);
        //更新位移
        distance = direction ? ((2 * radius + div) + (direction ? -radius : radius)) : (-(2 * radius + div) + (direction ? -radius : radius));
        //百分比
        mCurrentTime = position + positionOffset - (int) (position + positionOffset);
        if (!direction)
            mCurrentTime = 1 - mCurrentTime;
        if (Math.abs(lastCurrentTime - mCurrentTime) > 0.2) {
            if (lastCurrentTime < 0.1)
                mCurrentTime = 0;
            else if (lastCurrentTime > 0.9)
                mCurrentTime = 1;
        }
        lastCurrentTime = mCurrentTime;
        dispatchDraw(c);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        //绘制选中器
        LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
        int activePosition = layoutManager.findFirstVisibleItemPosition();
        if (activePosition == RecyclerView.NO_POSITION) {
            return;
        }
        // find offset of active page (if the user is scrolling)
        final View activeChild = layoutManager.findViewByPosition(activePosition);
        int left = activeChild.getLeft();
        int width = activeChild.getWidth();
        // on swipe the active item will be positioned from [-width, 0]
        // interpolate offset for smooth animation
        float progress = mInterpolator.getInterpolation(left * -1 / (float) width);
        //更新选中点
        updateDrop(c, activePosition, progress);
        Log.e(TAG, "方法:onDrawOver");
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        //获取个数
        indicatorCount = parent.getAdapter().getItemCount();
        //指示圆点所占的长度
        float totalLength = radius * 2 * indicatorCount;
        //间距所占长度
        float paddingBetweenItems = Math.max(0, indicatorCount - 1) * div;
        //指示器总长度
        float indicatorTotalWidth = totalLength + paddingBetweenItems;
        //居中指示器开始位置
        firstX = (parent.getWidth() - indicatorTotalWidth) / 2F;
        //高度的中点
        startY = parent.getHeight() - mIndicatorHeight / 2;
        //绘制指示器
        drawInactiveIndicators(c);
        Log.e(TAG, "方法:onDraw");
    }

    //绘制指示器
    private void drawInactiveIndicators(Canvas c) {
        for (int i = 0; i < indicatorCount; i++) {
            c.drawCircle(firstX + radius + i * (div + 2 * radius), startY, radius, indicatorPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //预留出指示器高度
        outRect.bottom = mIndicatorHeight;
        Log.e(TAG, "方法:getItemOffsets");
    }


    class XPoint {//用来存储X轴相等的做坐标点,这里用来存p2,p4,其中p2代表左面3个点,p4代表右面三个点
        public float x;//中间点X坐标
        public float y;//中间点Y坐标
        public float mc;//上下到中间的间距
        public PointF bottom;//下面点
        public PointF top;//上面点

        public XPoint(float x, float y, float mc) {
            this.x = x;
            this.y = y;
            this.mc = mc;
            if (bottom == null)
                bottom = new PointF();
            if (top == null)
                top = new PointF();
            bottom.y = y + mc;
            top.y = y - mc;
            bottom.x = x;
            top.x = x;
        }

        public void setMc(float mc) {
            this.mc = mc;
            bottom.y = y + mc;
            top.y = y - mc;
        }

        public void setY(float y) {
            this.y = y;
            bottom.y = y + mc;
            top.y = y - mc;
        }

        public void setX(float x) {
            this.x = x;
            bottom.x = x;
            top.x = x;
        }

        @Override
        public String toString() {
            return "XPoint{" +
                    "x=" + x +
                    ", y=" + y +
                    ", mc=" + mc +
                    ", bottom=" + bottom +
                    ", top=" + top +
                    '}';
        }
    }

    class YPoint {//用来存储Y轴相等的做坐标点,这里用来存p1,p3,其中p1代表上面3个点,p3代表下面三个点
        public float x;//中间点X坐标
        public float y;//中间点坐标
        public float mc;//左右距中点间距
        public PointF left;//左点
        public PointF right;//右点

        public YPoint(float x, float y, float mc) {
            this.x = x;
            this.y = y;
            this.mc = mc;
            if (left == null)
                left = new PointF();
            if (right == null)
                right = new PointF();
            right.x = x + mc;
            left.x = x - mc;
            left.y = y;
            right.y = y;
        }

        public void setMc(float mc) {
            this.mc = mc;
            right.x = x + mc;
            left.x = x - mc;
        }

        public void setX(float x) {
            this.x = x;
            right.x = x + mc;
            left.x = x - mc;
        }

        public void setY(float y) {
            this.y = y;
            left.y = y;
            right.y = y;
        }

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

推荐阅读更多精彩内容