安卓:自定义View实现底部滑动布局(可配合Recyclervi
2019-08-17 本文已影响35人
仓鼠说他不会飞
前言:
忙忙碌碌一个月,加入公司地图引擎更换的任务中,忙里偷闲记录下实现的一个低配底部滑动布局,原本计划用的是BottomSheet,可无奈,产品需求+控件使用环境,不得不自定义View来解决问题。
实现思路
实现思路相对坎坷,首次实现无法响应Recyclerview的滑动,在滑动拦截处理过后,算是完善了个版本。
实现的效果如下(重新写的damo):
使用方式
注意。。。。SlideBottomLayout只允许一个直接子布局,因为实现方式的原因,因为要使用SlideBottomLayout的子布局实现功能(如果看官觉得很鸡肋,就当我抛砖引玉)。
xml:
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/cat_back"/>
<com.example.testativity.SlideBottomLayout
android:id="@+id/slideBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:handler_height="100dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="500dp"
android:orientation="vertical"
android:background="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center">
<View
android:layout_width="60dp"
android:layout_height="5dp"
android:background="@drawable/top_gray_sign"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rc"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</com.example.testativity.SlideBottomLayout>
java:
SlideBottomLayout slideBottom = (SlideBottomLayout)findViewById(R.id.slideBottom);
RecyclerView rc = (RecyclerView)findViewById(R.id.rc);
slideBottom.bindRecyclerView(rc);
rc.setLayoutManager(new LinearLayoutManager(this));
List<String> list = new ArrayList<>();
for (int i = 0;i<30;i++){
list.add("时光不老,我们不散!");
}
final SlideBottomRcAdapter adapter = new SlideBottomRcAdapter(list,this);
adapter.setListener(new SlideBottomRcAdapter.MultipleChoiceListener() {
@Override
public void setEvent(int position) {
adapter.Update(position);
}
});
rc.setAdapter(adapter);
rc.setOverScrollMode(View.OVER_SCROLL_NEVER);
原理如下:测量滑动距离把布局画在滑动距离之下,通过onTouchEvent方法中对滑动事件进行判断。具体判断加注释已经在代码里面了。
@Override
public boolean onTouchEvent(MotionEvent event) {
final float dy = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (touchActionDown(dy)) {
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (touchActionMove(dy)) {
return true;
}
break;
case MotionEvent.ACTION_UP:
if (touchActionUp(dy)) {
return true;
}
break;
}
return super.onTouchEvent(event);
}
对不同情况下的判断,interceptTouchEvent方法进行拦截交予onTouchEvent处理
代码片段如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float interceptY = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
RecordY(interceptY);
break;
case MotionEvent.ACTION_MOVE:
if(interceptJudge(interceptY)){
return onTouchEvent(ev);
}
return false;
case MotionEvent.ACTION_UP:
if(interceptJudge(interceptY)){
return onTouchEvent(ev);
}
return false;
}
return super.onInterceptTouchEvent(ev);
}
贴出全部代码,内容注释清楚,以便下次观看一目了然(SlideBottomLayout类)
public class SlideBottomLayout extends LinearLayout {
/**
* 手势按下位置记录
*/
private float downY;
/**
* 手势移动位置记录
*/
private float moveY;
/**
* 手势移动距离
*/
private int movedDis;
/**
* 移动的最大值
*/
private int movedMaxDis;
/**
* SlideBottom 的子视图
*/
private View childView;
/**
* SlideBottom状态
* isShow的两种状态 伸张与收缩
*/
private Boolean isShow = false;
/**
* 状态切换阈值
*/
private float hideWeight = 0.3f;
/**
* 拦截器参数相关
* 记录Action.Down按下位置
* @param hideWeight
*/
private int CurrentY;
/**
* 视图滚动辅助
*/
private Scroller mScroller;
/**
*
* 标记:childView到达parent或者其他的顶部
*/
private boolean arriveTop = false;
/**
* 设置:childView的初始可见高度
*/
private float visibilityHeight;
/**
* 绑定的Rc
*/
private RecyclerView recyclerview;
private ShortSlideListener shortSlideListener;
public SlideBottomLayout(@NonNull Context context) {
super(context);
}
public SlideBottomLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
}
public SlideBottomLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs);
}
/**
*初始化属性配置
* @param context the {@link Context}
* @param attrs the configs in layout attrs.
*/
private void initAttrs(Context context, AttributeSet attrs) {
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideBottomLayout);
visibilityHeight = ta.getDimension(R.styleable.SlideBottomLayout_handler_height, 0);
ta.recycle();
initConfig(context);
}
/**
* 实现视图平滑滚动利器
* @param context
*/
private void initConfig(Context context) {
if (mScroller == null) {
mScroller = new Scroller(context);
}
}
/**
* 使用前判断/单一子视图
* 该方法在OnMeasure(int,int)调用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() == 0 || getChildAt(0) == null) {
throw new RuntimeException("SlideBottom里面没有子布局");
}
if (getChildCount() > 1) {
throw new RuntimeException("SlideBottom里只可以放置一个子布局");
}
childView = getChildAt(0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
movedMaxDis = (int) (childView.getMeasuredHeight() - visibilityHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
childView.layout(0, movedMaxDis, childView.getMeasuredWidth(), childView.getMeasuredHeight() + movedMaxDis);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float interceptY = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
RecordY(interceptY);
break;
case MotionEvent.ACTION_MOVE:
if(interceptJudge(interceptY)){
return onTouchEvent(ev);
}
return false;
case MotionEvent.ACTION_UP:
if(interceptJudge(interceptY)){
return onTouchEvent(ev);
}
return false;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 记录下拦截器传来的Y值
* @param interceptY
*/
private void RecordY(float interceptY) {
CurrentY = (int)interceptY;
}
/**
* 拦截判断
* @param interceptY
* @return
*/
private boolean interceptJudge(float interceptY) {
float judgeY = CurrentY - interceptY;
if(judgeY > 0){
//向上滑动
if(!arriveTop()){
return true;
}
}
if(judgeY < 0){
//向下滑动
if(arriveTop() && isTop(recyclerview)){
return true;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float dy = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (touchActionDown(dy)) {
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (touchActionMove(dy)) {
return true;
}
break;
case MotionEvent.ACTION_UP:
if (touchActionUp(dy)) {
return true;
}
break;
}
return super.onTouchEvent(event);
}
/**
* scroll的更新方法
* computeScrollOffset 返回true表示动画未完成
*/
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller == null)
mScroller = new Scroller(getContext());
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
public boolean touchActionUp(float eventY) {
//移动的位置是否大于阈值
if (movedDis > movedMaxDis * hideWeight) {
switchVisible();
} else {
//提供一个接口用于处理没有达到阈值的手势
if (shortSlideListener != null) {
shortSlideListener.onShortSlide(eventY);
} else {
hide();
}
}
return true;
}
public boolean touchActionMove(float eventY) {
moveY = eventY;
//dy是移动距离的和 如果它的值>0表示向上滚动 <0表示向下滚动
final float dy = downY - moveY;
if (dy > 0) { //向上
movedDis += dy;
if (movedDis > movedMaxDis) {
movedDis = movedMaxDis;
}
if (movedDis < movedMaxDis) {
scrollBy(0, (int) dy);
downY = moveY;
return true;
}
} else { //向下
movedDis += dy;
if (movedDis < 0) {
movedDis = 0;
}
if (movedDis > 0) {
scrollBy(0, (int)dy);
}
downY = moveY;
return true;
}
return false;
}
public boolean touchActionDown(float eventY) {
//记录手指按下的位置
downY = (int) eventY;
if (!arriveTop && downY < movedMaxDis) {
return false;
} else{
return true;
}
}
/**
* slidBottom的显示方法
*/
public void show() {
scroll2TopImmediate();
}
/**
* slidBottom的隐藏方法
*/
public void hide() {
scroll2BottomImmediate();
}
/**
* arriveTop返回值
* 判断child是否到达顶部
*/
public boolean switchVisible() {
if (arriveTop()) {
hide();
} else {
show();
}
return arriveTop();
}
public boolean arriveTop() {
return this.arriveTop;
}
public void scroll2TopImmediate() {
mScroller.startScroll(0, getScrollY(), 0, (movedMaxDis - getScrollY()));
invalidate();
movedDis = movedMaxDis;
arriveTop = true;
isShow= true;
}
public void scroll2BottomImmediate() {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
postInvalidate();
movedDis = 0;
arriveTop = false;
isShow = false;
}
/**
* 绑定Recyclerview如果你的子布局中含有Recyclerview的话
* 该方法用于判断是否到达Recyclerview的顶部
* @param recyclerView
* @return
*/
public static boolean isTop(RecyclerView recyclerView){
if(recyclerView == null){
return false;
}
return !recyclerView.canScrollVertically(-1);
}
/**
* 绑定RecyclerView(可选)
* 如果子布局有RecyclerView必须绑定否则Recyclerview的滑动不会被拦截
* @param recyclerView
*/
public void bindRecyclerView(RecyclerView recyclerView){
this.recyclerview = recyclerView;
}
public void setShortSlideListener(ShortSlideListener listener) {
this.shortSlideListener = listener;
}
/**
* 隐藏比重阈值
* @param hideWeight
*/
public void setHideWeight(float hideWeight) {
if (hideWeight <= 0 || hideWeight > 1) {
throw new IllegalArgumentException("隐藏的阈值应该在(0f,1f]之间");
}
this.hideWeight = hideWeight;
}
/**
* 设置显示高度
* @param visibilityHeight
*/
public void setVisibilityHeight(float visibilityHeight) {
this.visibilityHeight = visibilityHeight;
}
}
写在最后
关于阈值,是控制滑动距离展示隐藏的临界值,没有动态设置,简单的set方法或者attributeset都可以。
关于ListView使用,参考Recyclerview的使用。
对于点击顶部隐藏,如果之后要改只需要在OnTouchEvent中剔除即可。