仿小米相册列表实现自定义带快速索引功能的RecyclerView

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


篇章目标要点

`
RecyclerView是一种常用的列表容器,但是在展示大量内容时,如何快速找到我们想要定位的内容在设计中一般都很必要考虑的。比如在小米手机的本地相册展示时就使用了一个快速引用的,通过拖动索引浮标就可以快速找到对应位置。本文参照小米相册尝试实现一种带快速索引功能的RecyclerView。

一、实现效果

  1. 列表滑动时,浮标可以指示当前列表已经翻动的位置
    请添加图片描述

  2. 浮标滑动时,列表可以滚动到对应比例位置,并且浮标上展示了当前位置序号和关键信息
    请添加图片描述

二、设计布局原理

   考虑浮标与RecyclerView是处于叠加关系,计划增加一个FrameLayout作为父类容器,方案基本思路就是实现浮标跟随手指滑动在父类容器中跟随调整位置,并且根据纵向滑动的百分比调整RecyclerView目标显示位置。

在这里插入图片描述

三、关键代码实现

1.浮标随手势移动

在视图layout完成时获取浮标在父容器当中的x,y坐标

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    fetchMargin();
}

/**
 * 获取浮标父布局在父容器当中的x,y坐标
 */
private void fetchMargin(){
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)mLayoutShortcut.getLayoutParams();
    mMarginX = mLayoutShortcut.getX();
    mMarginY = mLayoutShortcut.getY();
    Log.d(TAG, "initial margin x = "+mMarginX+", y = "+mMarginY+",right = "+lp.rightMargin);
}

在MOVE事件中设置浮标跟随移动,并且设置列表的滚动目标位置。在UP事件中检查浮标是否存在偏移,如存在偏移,则恢复浮标右侧位置,并且设置手指抬起后3s自动收起浮标。

//记录滑动事件的过程坐标和按下坐标
private float x , y , mInitX, mInitY;
@SuppressLint("ClickableViewAccessibility")
//监听浮标布局的touch事件并执行随手移动和滚动列表
private void handleTouchEvent(){
    mLayoutShortcut.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            x = event.getX();
            y = event.getY();
            mStatusState[1] = event.getAction();
            Log.d(TAG, "event x = " + x + ", y = "+y+",action ="+event.getAction());
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    //如原来有待处理的延迟关闭浮标的任务,先取消
                    mButtonInTouch = true;
                    if(mTextViewTitle.getVisibility() == View.INVISIBLE){
                        mTextViewTitle.setVisibility(View.VISIBLE);
                    }
                    if(null != mDispose && !mDispose.isDisposed()){
                        mDispose.dispose();
                    }
                    mInitX = x;
                    mInitY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //执行浮标跟随手指拖动及列表滚动
                    mButtonInTouch = true;
                    locateFloatButtonByTouch(x - mInitX, y - mInitY);
                    fastScrollList(v.getY() - mInitY);
                    break;
                case MotionEvent.ACTION_UP:
                    //如浮标有出现横向偏移,抬起时恢复默认的右侧位置
                    mButtonInTouch = false;
                    recoveryFloatButton();
                    hideFloatButton();
                    fetchMargin();
                    break;
                case MotionEvent.ACTION_CANCEL:
                    mButtonInTouch = false;
                default:
                    break;
            }
            return true;
        }
    });

调整浮标位置

/**
 * 拖动浮标时调整浮标的位置
 * @param x touch位置的x坐标,即shiftButton中心x坐标
 * @param y touch位置的y坐标,即shiftButton中心y坐标
 */
private void locateFloatButtonByTouch(float x, float y){
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)mLayoutShortcut.getLayoutParams();
    Log.d(TAG, "locate new x = "+(mMarginX + x)+", new y = "+mMarginY+", origin x = "+lp.leftMargin+", origin y = "+y);
    int afterX = (int)(mMarginX + x);
    int afterY = (int)(mMarginY + y);
    if(afterX > mWidth){
        afterX = mWidth;
    }else if(afterX <= 0){
        afterX = 0;
    }
    if(afterY > (mHeight - mFloatButtonHeight)){
        afterY = (int)(mHeight - mFloatButtonHeight);
    }else if(afterY <= 0){
        afterY = 0;
    }
    lp.topMargin = afterY;
    mLayoutShortcut.setLayoutParams(lp);
    mLayoutShortcut.setX(afterX);
}

抬起时恢复浮标位置

/**
 * 如果悬浮按钮被拖动至了非默认位置,需要首先执行位置恢复动效
 */
private void recoveryFloatButton(){
    Log.d(TAG, "recovery float button");
    int diff = (int)((mWidth - mLayoutShortcut.getWidth() -DEFAULT_MARGIN_RIGHT) - mLayoutShortcut.getX());
    int initial = (int)mLayoutShortcut.getX();
    if(diff != 0){
        int MAX_VALUE = 10;
        ValueAnimator vm = ValueAnimator.ofInt(MAX_VALUE).setDuration(800);
        vm.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float x = initial + diff * (int)animation.getAnimatedValue() / MAX_VALUE;
                mLayoutShortcut.setX(x);
            }
        });
        vm.start();
    }
}

没有滑动时,超时自动隐藏浮标

/**
 * 没有滑动时,超过5s后自动隐藏
 */
private void hideFloatButton(){
    if(null != mDispose && !mDispose.isDisposed()){
        mDispose.dispose();
    }
    if(mTextViewTitle.getVisibility() == View.VISIBLE){
        mDispose = Schedulers.computation().scheduleDirect(new Runnable() {
            @Override
            public void run() {
                Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.anim_exit_to_right);
                mTextViewTitle.setAnimation(animation);
                mTextViewTitle.startAnimation(animation);
                Log.d(TAG,"anim begin");
                animation.setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {
                        Log.d(TAG,"anim start");
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        Log.d(TAG,"anim end");
                        mTextViewTitle.setVisibility(INVISIBLE);
                        mImageViewIcon.setVisibility(INVISIBLE);
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {

                    }
                });
            }
        }, 3, TimeUnit.SECONDS);
    }else{
        mDispose = Schedulers.computation().scheduleDirect(new Runnable() {
            @Override
            public void run() {
                mImageViewIcon.setVisibility(INVISIBLE);
            }
        }, 3, TimeUnit.SECONDS);
    }
}

2.浮标随列表移动

列表移动,浮标移动可以标记当前可见位置居于整体列表的大概位置。跟随列表移动时,仅调整y向位置即可。

/**
 * 在RecyclerView滑动时调整浮标位置
 * @param y 浮标中心的y坐标
 */
private void locateFloatButtonByScroll(float y){
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams)mLayoutShortcut.getLayoutParams();
    mMarginY = y - mFloatButtonHeight / 2;
    lp.topMargin = (int)mMarginY;
    mLayoutShortcut.setLayoutParams(lp);
}

3.列表随浮标移动而滚动

floatButton Touch时快速滚动RecyclerView,根据滑动位置占据视图高度的百分比计算目标列表位置,设置列表目标位置居中显示。

private void fastScrollList(float deltaY){
    float percentY = 1.00f * (deltaY - mFloatButtonHeight / 2) / (mHeight - mFloatButtonHeight);
    mRecyclerView.fastScrollList(percentY);
}

在自定义RecyclerView中执行滑动逻辑,并且实时回调当前浮标位置对应的位置和关键信息。

/**
 * 根据滑动位置占据视图的百分比,计算目标移动到列表的位置
 * @param percentY
 */
public void fastScrollList(float percentY){
    int count = getAdapter().getItemCount();
    int pos = (int)(percentY * count);
    Log.d(TAG,"scroll to position = "+pos+", percentY = "+percentY +",count = "+count);
    if(pos >= 0){
        smoothScrollToPosition(pos);
        //设置浮标显示内容,显示序号+内容,可以根据需求自行定义
        TrackBean trackBean = ((SongAdapter)(getAdapter())).getDataList().get(pos);
        mScrollState.onCurrentPos(pos, trackBean);
    }
}

学习心得

   文中Demo给出了一个实现思路和基本功能完成,距离量产质量还需要更多的测试和优化,也欢迎大家反馈意见和多多交流。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值