底部可拖动列表

需求

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);
        }
    }

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