自定义控件

底部可拖动列表

2020-09-30  本文已影响0人  风吹尘埃

需求

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

image.png
image2.gif

实现思路

1.选定实现方式

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

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

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

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.可拖动

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

}
上一篇下一篇

猜你喜欢

热点阅读