底部可拖動列表

需求

1.列表顯示在底部
2.填充一個列表
3.點擊"展開","收起"執(zhí)行展開收起的動畫并將列表展開和收起
4."展開","收起"的按鈕按住可以拖動
5.拖動有邊界值,最高為屏幕高度的0.3,最低為 屏幕高度 - "展開"按鈕的高度
6.動態(tài)添加item
如下圖

image.png

image2.gif

實現(xiàn)思路

1.選定實現(xiàn)方式

  • 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 事件分發(fā)實例之可拖動的ViewGroup

  • 1.BottomListWindowView自身是FrameLayout,再addView添加LinearLayout
  • 2.通過onInterceptTouchEvent分發(fā)觸摸事件,當手指觸摸在"展開"按鈕上且判定為滑動才攔截
  • 3.在onTouchEvent中通過setY()執(zhí)行拖拽

以上實際使用后不行,setY()是修改Layout的位置,即整個Layout向下平移,這樣會使RecyclerView的Item被遮擋
需要使Layout的底部固定在屏幕的底部,然后動態(tài)修改Layoutheight

5.點擊按鈕執(zhí)行展開關閉的動畫

  • 使用ObjectAnimator修改自身的translationY

6.動態(tài)添加Item

流程

1.畫個在底部的列表

布局不難,使用FrameLayout將自己的xml文件添加進里面,再將View放到Activity的地步就好了。這里View需要填滿屏幕

注意背景有陰影,但硬件公司沒有UI,所以只能自己畫


image.png
  • 自定義陰影

自定義View實現(xiàn)陰影
自定義View-第十四步:setShadowLayer陰影與SetMaskFilter發(fā)光效果

使用layer-list畫背景,感覺效果不是很好
于是使用自定義Drawable
自定義Drawable實現(xiàn)方式有兩種
一種是使用PaintsetShadowLayer設置陰影
一種是使用PaintsetMaskFilter設置蒙版

使用setShadowLayer感覺效果不是很好于是選擇setMaskFilter
具體使用方式看這里
代碼如下

public class ShadowDrawable extends Drawable {

    private final Paint paint;
    private int width;
    private int height;
    // 陰影顏色
    private int shadowColor = Color.BLACK;
    // 背景(內(nèi)容區(qū)域)顏色
    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會將狀態(tài)欄和導航欄也計算在內(nèi),導致偏移,所以需要只獲取布局的高
獲取方式有兩種
一種是在onMeasure方法中測量
一種是調(diào)Viewpost方法,該方法傳入的Runnable會在View添加進ViewGroup后被執(zhí)行
這里用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滑動到底部 關閉狀態(tài)
        ViewGroup.LayoutParams layoutParams = BottomListWindowView.this.getLayoutParams();
        layoutParams.height = closeHeight;
        BottomListWindowView.this.setLayoutParams(layoutParams);
        nowHeight = closeHeight;
        isOpen = false;
    }
});

3.可拖動

  • 使用onInterceptTouchEvent做事件分發(fā)
    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 為系統(tǒng)的值
        // minTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        intercept = Math.abs(dy) > minTouchSlop;
    }

    return intercept;
}
  • onTouchEvent中執(zhí)行拖拽
    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.點擊按鈕執(zhí)行動畫

這個比較簡單,使用ObjectAnimator執(zhí)行translationY的動畫就好了
nowY為當前的Y值,這樣拖動到一半點擊按鈕就可以從當前位置開始執(zhí)行動畫

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) {
                // 動態(tài)修改高度
                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滑動到底部 關閉狀態(tài)
                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);

    }

    /**
     * 點擊 "展開" 按鈕判斷并執(zhí)行相應動畫
     */
    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) {
                    // 動態(tài)修改高度
                    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;
    }

    /**
     * 事件分發(fā)
     */
    @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);
        }
    }

}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容