美文网首页自定义控件
底部可拖动列表

底部可拖动列表

作者: 风吹尘埃 | 来源:发表于2020-09-30 10:56 被阅读0次

    需求

    1.列表显示在底部
    2.填充一个列表
    3.点击"展开","收起"执行展开收起的动画并将列表展开和收起
    4."展开","收起"的按钮按住可以拖动
    5.拖动有边界值,最高为屏幕高度的0.3,最低为 屏幕高度 - "展开"按钮的高度
    6.动态添加item
    如下图

    image.png
    image2.gif

    实现思路

    1.选定实现方式

    • Dialog: 没法常驻(百度没搜到)
    • PopupWindow: 没法常驻(百度没搜到)
    • 自定义View

    2.画个在底部的列表

    // Activity布局结构
    <androidx.constraintlayout.widget.ConstraintLayout>
    
        <com.widget.BottomListWindowView
            android:id="@+id/bottom_list_window_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    // BottomListWindowView布局结构
    <LinearLayout>
        <TextView />
        <View />
        <androidx.recyclerview.widget.RecyclerView />
    </LinearLayout>
    

    3.计算边界值

    • 判断最高和最低的位置

    4.拖拽

    Android 事件分发实例之可拖动的ViewGroup

    • 1.BottomListWindowView自身是FrameLayout,再addView添加LinearLayout
    • 2.通过onInterceptTouchEvent分发触摸事件,当手指触摸在"展开"按钮上且判定为滑动才拦截
    • 3.在onTouchEvent中通过setY()执行拖拽

    以上实际使用后不行,setY()是修改Layout的位置,即整个Layout向下平移,这样会使RecyclerView的Item被遮挡
    需要使Layout的底部固定在屏幕的底部,然后动态修改Layoutheight

    5.点击按钮执行展开关闭的动画

    • 使用ObjectAnimator修改自身的translationY

    6.动态添加Item

    流程

    1.画个在底部的列表

    布局不难,使用FrameLayout将自己的xml文件添加进里面,再将View放到Activity的地步就好了。这里View需要填满屏幕

    注意背景有阴影,但硬件公司没有UI,所以只能自己画


    image.png
    • 自定义阴影

    自定义View实现阴影
    自定义View-第十四步:setShadowLayer阴影与SetMaskFilter发光效果

    使用layer-list画背景,感觉效果不是很好
    于是使用自定义Drawable
    自定义Drawable实现方式有两种
    一种是使用PaintsetShadowLayer设置阴影
    一种是使用PaintsetMaskFilter设置蒙版

    使用setShadowLayer感觉效果不是很好于是选择setMaskFilter
    具体使用方式看这里
    代码如下

    public class ShadowDrawable extends Drawable {
    
        private final Paint paint;
        private int width;
        private int height;
        // 阴影颜色
        private int shadowColor = Color.BLACK;
        // 背景(内容区域)颜色
        private int backColor = Color.WHITE;
        // 阴影大小
        private int shadowSize = 0;
        // 圆角
        private int radius = 0;
    
        public ShadowDrawable(int width, int height) {
    
            paint = new Paint(Paint.ANTI_ALIAS_FLAG);
            paint.setStrokeWidth(5);
            paint.setStyle(Paint.Style.FILL);
    
            this.width = width;
            this.height = height;
        }
    
        @Override
        public void draw(@NonNull Canvas canvas) {
            RectF rect=new RectF(0,shadowSize,width,height);
            if (shadowSize > 0){
                paint.setColor(shadowColor);
                paint.setMaskFilter(new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.NORMAL));
                canvas.drawRoundRect(rect,radius,radius,paint);
            }
    
            paint.setColor(backColor);
            paint.setMaskFilter(null);
            canvas.drawRoundRect(rect,radius,radius,paint);
    
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
                // 低于5.0的版本无效,画个圈代替吧
                paint.setStrokeWidth(0.1f);
                paint.setColor(shadowColor);
                paint.setStyle(Paint.Style.STROKE);
                canvas.drawRoundRect(rect,radius,radius,paint);
            }
    
        }
    
        public ShadowDrawable setRadius(int radius) {
            this.radius = radius;
            return this;
        }
    
        public ShadowDrawable setShadowColor(int shadowColor) {
            this.shadowColor = shadowColor;
            return this;
        }
        public ShadowDrawable setBackColor(int backColor) {
            this.backColor = backColor;
            return this;
        }
    
        public ShadowDrawable setShadowSize(int shadowSize) {
            this.shadowSize = shadowSize;
            return this;
        }
    
        /**
         * 使重绘
         */
        public void invalidate(){
            invalidateSelf();
        }
    
        @Override
        public void setAlpha(int alpha) {}
    
        @Override
        public void setColorFilter(@Nullable ColorFilter colorFilter) {}
        @Override
        public int getOpacity() {
            return PixelFormat.TRANSLUCENT;
        }
    }
    

    2.判断边界值

    最高为屏幕高的0.3
    这里如果直接获取WindowManagerheightPixels会将状态栏和导航栏也计算在内,导致偏移,所以需要只获取布局的高
    获取方式有两种
    一种是在onMeasure方法中测量
    一种是调Viewpost方法,该方法传入的Runnable会在View添加进ViewGroup后被执行
    这里用post方法

    post(new Runnable() {
        @Override
        public void run() {
            int layoutHeight = BottomListWindowView.this.getHeight();
            titleHeight = tvUnfoldList.getHeight();
            // 初始化最大高度  为总高度的0.7
            openHeight = (int)(layoutHeight * 0.7);
            // 初始化最小高度  为"展开"按钮的高度
            closeHeight = titleHeight;
            // 使View滑动到底部 关闭状态
            ViewGroup.LayoutParams layoutParams = BottomListWindowView.this.getLayoutParams();
            layoutParams.height = closeHeight;
            BottomListWindowView.this.setLayoutParams(layoutParams);
            nowHeight = closeHeight;
            isOpen = false;
        }
    });
    

    3.可拖动

    • 使用onInterceptTouchEvent做事件分发
      1.判断点击位置为展开按钮
      2.判断Y轴上的滑动距离超过最小距离,则判断为滑动
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
    
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // down事件获取down的位置
            downX = ev.getX();
            downY = ev.getY();
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            // 判断 down 的位置不为 "展开" 按钮的位置则不拦截
            if (!(downX > tvUnfoldList.getLeft() && downX < tvUnfoldList.getRight() && downY > tvUnfoldList.getTop() && downY < tvUnfoldList.getBottom())) {
                return false;
            }
            // 获取滑动距离
            float dy = ev.getY() - downY;
            // 大于最小距离,判定为滑动
            // minTouchSlop 为系统的值
            // minTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
            intercept = Math.abs(dy) > minTouchSlop;
        }
    
        return intercept;
    }
    
    • onTouchEvent中执行拖拽
      1. 计算应该滑动的Y值
      2. 判断边界值
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_MOVE) {
            float moveY = event.getY();
            // getY() 获取当前的Y值
            // moveY - downY 得到滑动的距离
            float endY = (moveY - downY);
    
            // 使用 setY 只是改变Layout的位置,向下移动的话 RecyclerView 会被挡住导致看不到底下的item
    //            setY(endY);
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            int height = (int) (layoutParams.height - endY);
    
            // 判断是否达到边界值
            if (height <= closeHeight) {
                height = closeHeight;
            } else if (height >= openHeight) {
                height = openHeight;
            }
            // 改变 Layout 的高度
            layoutParams.height = (int) height;
            setLayoutParams(layoutParams);
            nowHeight = height;
    
    
            if (nowHeight == closeHeight && isOpen){
                unfoldBtText = "展开";
                setUnfoldText();
                isOpen = false;
            }else if (nowHeight != closeHeight && !isOpen){
                unfoldBtText = "收起";
                setUnfoldText();
                isOpen = true;
            }
        }
        return true;
    }
    

    4.点击按钮执行动画

    这个比较简单,使用ObjectAnimator执行translationY的动画就好了
    nowY为当前的Y值,这样拖动到一半点击按钮就可以从当前位置开始执行动画

    private void switchStateAnim() {
        if (isOpen) {
            unfoldBtText = "展开";
            setUnfoldText();
            isOpen = false;
    
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(nowHeight, closeHeight);
            valueAnimator.setDuration(300);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    // 动态修改高度
                    float value = (float) animation.getAnimatedValue();
                    ViewGroup.LayoutParams layoutParams = getLayoutParams();
                    layoutParams.height = (int) value;
                    setLayoutParams(layoutParams);
                }
            });
            valueAnimator.start();
    
            nowHeight = closeHeight;
        } else {
            unfoldBtText = "收起";
            setUnfoldText();
            isOpen = true;
            nowHeight = openHeight;
    
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(closeHeight, nowHeight);
            valueAnimator.setDuration(300);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float value = (float) animation.getAnimatedValue();
                    ViewGroup.LayoutParams layoutParams = BottomListWindowView.this.getLayoutParams();
                    layoutParams.height = (int) value;
                    BottomListWindowView.this.setLayoutParams(layoutParams);
                }
            });
            valueAnimator.start();
        }
    }
    

    5.添加Item

    当RecyclerView的Item为0时,列表会收缩,这样当点击展开按钮出来的就一个透明背景,只有展开按钮
    像这样

    image.png
    所以需要通过测量修改大小
    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 设置宽高
            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.getSize(heightMeasureSpec));
            // 设置最小高度,这样当RecyclerView的Item为0时,也能填满屏幕
            inflate.setMinimumHeight(MeasureSpec.getSize(heightMeasureSpec));
        }
    

    整体代码

    public class BottomListWindowView extends FrameLayout {
    
        private boolean isOpen = false;
        private float minTouchSlop;
        private TextView tvUnfoldList;
        private int closeHeight;
        private int openHeight;
        private float nowHeight;
        private FirmwareFileListAdapter firmwareFileListAdapter;
        private String unfoldBtText = "展开";
        private View inflate;
        float downX = 0;
        float downY = 0;
        private int titleHeight;
    
    
        public BottomListWindowView(Context context) {
            this(context, null);
        }
    
        public BottomListWindowView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public BottomListWindowView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            // 最小滑动距离
            minTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    
            // 列表
            inflate = LayoutInflater.from(context).inflate(R.layout.dialog_firmware_file_list, this, false);
    
            // 阴影背景
            ShadowDrawable shadowDrawable = new ShadowDrawable(WindowUtils.getWindowWidth(getContext()), WindowUtils.getWindowHeight(getContext()));
            inflate.setBackground(shadowDrawable);
    
            int backColor = ContextCompat.getColor(getContext(), R.color.white);
            int shadowColor = ContextCompat.getColor(getContext(), R.color.color_D5D0D0);
            shadowDrawable.setBackColor(backColor)
                    .setShadowColor(shadowColor)
                    .setShadowSize(20)
                    .setRadius(20).invalidate();
    
    
            tvUnfoldList = inflate.findViewById(R.id.tv_unfold_list);
    
            tvUnfoldList.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    switchStateAnim();
                }
            });
    
            RecyclerView rvFirmwareFileList = inflate.findViewById(R.id.rv_firmware_file_list);
            rvFirmwareFileList.setLayoutManager(new LinearLayoutManager(context));
            rvFirmwareFileList.addItemDecoration(new DividerItemDecoration(context, LinearLayoutManager.VERTICAL));
    
            firmwareFileListAdapter = new FirmwareFileListAdapter();
            firmwareFileListAdapter.setOnItemClickListener(new BaseAdapter.OnItemClickListener<FirmwareFileBean>() {
                @Override
                public void clickItem(View v, FirmwareFileBean firmwareFileBean, int position) {
                    if (onSelectedListener != null){
                        onSelectedListener.selected(firmwareFileBean);
                    }
                }
            });
    
            rvFirmwareFileList.setAdapter(firmwareFileListAdapter);
    
            addView(inflate);
    
            // 获取"展开"按钮的高度
            post(new Runnable() {
                @Override
                public void run() {
                    int layoutHeight = BottomListWindowView.this.getHeight();
                    titleHeight = tvUnfoldList.getHeight();
                    // 初始化最大高度  为总高度的0.7
                    openHeight = (int)(layoutHeight * 0.7);
                    // 初始化最小高度  为"展开"按钮的高度
                    closeHeight = titleHeight;
                    // 使View滑动到底部 关闭状态
                    ViewGroup.LayoutParams layoutParams = BottomListWindowView.this.getLayoutParams();
                    layoutParams.height = closeHeight;
                    BottomListWindowView.this.setLayoutParams(layoutParams);
                    nowHeight = closeHeight;
                    isOpen = false;
                }
            });
        }
    
        private OnSelectedListener onSelectedListener;
    
        public void setOnSelectedListener(OnSelectedListener onSelectedListener) {
            this.onSelectedListener = onSelectedListener;
        }
    
        public interface OnSelectedListener{
            void selected(FirmwareFileBean firmwareFileBean);
        }
    
        /**
         * 添加Item
         * @param data item
         */
        public void addData(FirmwareFileBean data){
            firmwareFileListAdapter.addData(data);
            setUnfoldText();
        }
    
        public void clearData(){
            firmwareFileListAdapter.clearData();
            setUnfoldText();
        }
    
        /**
         * 修改 "展开" 按钮文本
         */
        public void setUnfoldText(){
            int itemCount = firmwareFileListAdapter.getItemCount();
            String str = unfoldBtText + "(" + itemCount + ")";
            tvUnfoldList.setText(str);
    
        }
    
        /**
         * 点击 "展开" 按钮判断并执行相应动画
         */
        private void switchStateAnim() {
            if (isOpen) {
                unfoldBtText = "展开";
                setUnfoldText();
                isOpen = false;
    
                ValueAnimator valueAnimator = ValueAnimator.ofFloat(nowHeight, closeHeight);
                valueAnimator.setDuration(300);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        // 动态修改高度
                        float value = (float) animation.getAnimatedValue();
                        ViewGroup.LayoutParams layoutParams = getLayoutParams();
                        layoutParams.height = (int) value;
                        setLayoutParams(layoutParams);
                    }
                });
                valueAnimator.start();
    
                nowHeight = closeHeight;
            } else {
                unfoldBtText = "收起";
                setUnfoldText();
                isOpen = true;
                nowHeight = openHeight;
    
                ValueAnimator valueAnimator = ValueAnimator.ofFloat(closeHeight, nowHeight);
                valueAnimator.setDuration(300);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float value = (float) animation.getAnimatedValue();
                        ViewGroup.LayoutParams layoutParams = BottomListWindowView.this.getLayoutParams();
                        layoutParams.height = (int) value;
                        BottomListWindowView.this.setLayoutParams(layoutParams);
                    }
                });
                valueAnimator.start();
            }
        }
    
        /**
         * 打开
         * @param coefficient 0-1的值,使列表展开到最大值得 百分之coefficient
         */
        public void open(float coefficient){
            unfoldBtText = "收起";
            float openHeight = this.openHeight * coefficient;
            setUnfoldText();
            isOpen = true;
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(nowHeight,openHeight);
            valueAnimator.setDuration(300);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float value = (float) animation.getAnimatedValue();
                    ViewGroup.LayoutParams layoutParams = BottomListWindowView.this.getLayoutParams();
                    layoutParams.height = (int) value;
                    BottomListWindowView.this.setLayoutParams(layoutParams);
                }
            });
            valueAnimator.start();
            nowHeight = openHeight;
        }
    
        /**
         * 事件分发
         */
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept = false;
    
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                // down事件获取down的位置
                downX = ev.getX();
                downY = ev.getY();
            } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
                // 判断 down 的位置不为 "展开" 按钮的位置
                if (!(downX > tvUnfoldList.getLeft() && downX < tvUnfoldList.getRight() && downY > tvUnfoldList.getTop() && downY < tvUnfoldList.getBottom())) {
                    return false;
                }
                // 获取滑动距离
                float dy = ev.getY() - downY;
                // 大于最小距离,判定为滑动
                intercept = Math.abs(dy) > minTouchSlop;
            }
    
            return intercept;
        }
    
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_MOVE) {
                float moveY = event.getY();
                // getY() 获取当前的Y值
                // moveY - downY 得到滑动的距离
                float endY = (moveY - downY);
    
                // 使用 setY 只是改变Layout的位置,向下移动的话 RecyclerView 会被挡住导致看不到底下的item
        //            setY(endY);
                ViewGroup.LayoutParams layoutParams = getLayoutParams();
                int height = (int) (layoutParams.height - endY);
    
                // 判断是否达到边界值
                if (height <= closeHeight) {
                    height = closeHeight;
                } else if (height >= openHeight) {
                    height = openHeight;
                }
                // 改变 Layout 的高度
                layoutParams.height = (int) height;
                setLayoutParams(layoutParams);
                nowHeight = height;
    
    
                if (nowHeight == closeHeight && isOpen){
                    unfoldBtText = "展开";
                    setUnfoldText();
                    isOpen = false;
                }else if (nowHeight != closeHeight && !isOpen){
                    unfoldBtText = "收起";
                    setUnfoldText();
                    isOpen = true;
                }
            }
            return true;
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 设置宽高
            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), (int) (MeasureSpec.getSize(heightMeasureSpec)));
            // 设置最小高度,这样当RecyclerView的Item为0时,也能填满屏幕
            inflate.setMinimumHeight((int) (MeasureSpec.getSize(heightMeasureSpec)));
        }
    
        private static class FirmwareFileListAdapter extends BaseAdapter<FirmwareFileBean> {
    
            @Override
            public int createItem(int viewType) {
                return R.layout.item_firmware_file;
            }
    
            @Override
            public void bindData(@NonNull BaseViewHolder holder, int position) {
                FirmwareFileBean itemData = getItemData(position);
    
                TextView tvFileName = holder.getView(R.id.tv_file_name);
                TextView tvFilePath = holder.getView(R.id.tv_file_path);
                TextView tvFileSize = holder.getView(R.id.tv_file_size);
                TextView tvFileModifyTime = holder.getView(R.id.tv_file_modify_time);
                tvFileName.setText(itemData.getFileName());
                tvFilePath.setText(itemData.getFilePath());
                tvFileSize.setText(itemData.getFileSize() + "b");
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm");
                String format = simpleDateFormat.format(new Date(itemData.getLastModifiedTime()));
                tvFileModifyTime.setText(format);
            }
        }
    
    }
    

    相关文章

      网友评论

        本文标题:底部可拖动列表

        本文链接:https://www.haomeiwen.com/subject/ftoeuktx.html