底部可拖动列表
需求
1.列表显示在底部
2.填充一个列表
3.点击"展开","收起"执行展开收起的动画并将列表展开和收起
4."展开","收起"的按钮按住可以拖动
5.拖动有边界值,最高为屏幕高度的0.3,最低为 屏幕高度 - "展开"按钮的高度
6.动态添加item
如下图
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.拖拽
1.BottomListWindowView
自身是FrameLayout
,再addView添加LinearLayout
2.通过onInterceptTouchEvent
分发触摸事件,当手指触摸在"展开"按钮上且判定为滑动才拦截3.在onTouchEvent
中通过setY()
执行拖拽
以上实际使用后不行,setY()
是修改Layout
的位置,即整个Layout
向下平移,这样会使RecyclerView
的Item被遮挡
需要使Layout
的底部固定在屏幕的底部,然后动态修改Layout
的height
5.点击按钮执行展开关闭的动画
- 使用
ObjectAnimator
修改自身的translationY
6.动态添加Item
流程
1.画个在底部的列表
布局不难,使用FrameLayout
将自己的xml文件添加进里面,再将View放到Activity的地步就好了。这里View需要填满屏幕
注意背景有阴影,但硬件公司没有UI,所以只能自己画
image.png
- 自定义阴影
使用layer-list
画背景,感觉效果不是很好
于是使用自定义Drawable
自定义Drawable实现方式有两种
一种是使用Paint
的setShadowLayer
设置阴影
一种是使用Paint
的setMaskFilter
设置蒙版
使用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
这里如果直接获取WindowManager
的heightPixels
会将状态栏和导航栏也计算在内,导致偏移,所以需要只获取布局的高
获取方式有两种
一种是在onMeasure
方法中测量
一种是调View
的post
方法,该方法传入的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
中执行拖拽- 计算应该滑动的Y值
- 判断边界值
@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时,列表会收缩,这样当点击展开按钮出来的就一个透明背景,只有展开
按钮
像这样
所以需要通过测量修改大小
@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);
}
}
}