Android 自定义底部上拉控件的实现方法

作者:却把清梅嗅 时间:2023-08-18 18:41:19 

前言

又到了新的一月,今天提供一个Android自定义底部上拉布局的实现,起因是自己在项目中需要实现这样一个控件,干脆自己写一个练练手。

写完了觉得能想到的需求都基本有了(可能会有其它需求,不过基本上改吧改吧就行了),又花了一点时间直接放到了Github上托管,希望能给您一些参考价值:

SlideBottomLayout-Android 简单易上手的Android底部上拉控件

先看一下实现效果:

Android 自定义底部上拉控件的实现方法

分析一下这种控件的基本需求有以下几种:

1.有一个部分是能够作为把手(就是图中的handle,)进行拖拽的,这部分高度是暴露在界面中的 -> 需要实现:Handle按钮

* 特殊需求特殊分析,比如让这个Handle透明实现无Handle的效果

2.底部上啦布局是有一定高度限制的,不一定覆盖设备的整个屏幕 -> 需要自定义最大高度

3.当从底部上拉一点点时抬手,布局缩回,若超过一定高度,自动弹到最高,隐藏同理 -> 需要自定义自动到达顶部/隐藏的阈值

直接使用

直接使用也很简单,笔者进行了简单的封装,以供参考:

1. 在Project的build.gradle文件中添加:


allprojects {
repositories {
 ...
 maven { url 'https://jitpack.io' }
}
}

2.在Module的build.gradle文件中添加:


dependencies {
  compile 'com.github.qingmei2:SlideBottomLayout-Android:1.2.3'
}

3.Add view in your layout:

需要注意的是:为了简单实现,笔者偷了个懒,设定为该布局下只能有一个直接的子View(类似ScrollView)

因此如果您添加需要一个布局,请在外面嵌套一个ViewGroup:


<com.qingmei2.library.SlideBottomLayout
   android:id="@+id/slideLayout"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_alignParentBottom="true"
   android:layout_marginTop="200dp"
   app:handler_height="50dp">
   <!--app:handler_height:该属性就是您要暴露出来Handle的高度,详见下方的TextView(id=handle)-->
   <!--Just one child-->
   <LinearLayout
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical">
     <TextView
       android:id="@+id/handle"
       android:layout_width="match_parent"
       android:layout_height="50dp"
       android:background="#d95858"
       android:gravity="center"
       android:text="handle"
       android:textColor="@android:color/white"
       android:textSize="16sp" />
     <android.support.v7.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content">
     </android.support.v7.widget.RecyclerView>
   </LinearLayout>
 </com.qingmei2.library.SlideBottomLayout>

实现步骤

具体代码如下,其中上述需求的设置方式都已经在代码中声明:

先看下属性声明:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 <declare-styleable name="SlideBottomLayout">
   <attr name="handler_height" format="dimension"></attr>
 </declare-styleable>
</resources>

public class SlideBottomLayout extends LinearLayout {
 public void setShortSlideListener(ShortSlideListener listener) {
   this.shortSlideListener = listener;
 }
 private ShortSlideListener shortSlideListener;
 /**
  * The {@link MotionEvent#ACTION_DOWN} gesture location.
  */
 private int downY;
 /**
  * The {@link MotionEvent#ACTION_MOVE} gesture location.
  */
 private int moveY;
 /**
  * the value of moved distance by the gesture. When the value was modified and not exceed
  * the {@link #movedMaxDis}, then make this ViewGroup move.
  */
 private int movedDis;
 /**
  * The max distance that the {@link SlideBottomLayout} can scroll to, it used to compare with the
  * {@link #downY}, determine whether it can slide by the gesture.
  */
 private int movedMaxDis;
 /**
  * ChildView of the {@link SlideBottomLayout}, you can set a Layout such as the {@link LinearLayout}、
  * {@link android.widget.RelativeLayout} ect.
  * We set the rules that {@link SlideBottomLayout} just can have one child-view, or else get a
  * {@link RuntimeException} at {@link #onFinishInflate()}
  */
 private View childView;
 /**
  * The control {@link SlideBottomLayout} automatically switches the threshold of the state. if
  * this ViewGroup moved distance more than {@link #movedMaxDis} * it, switch the state of
  * {@link #arriveTop} right now.
  * </p>
  * See the {@link #touchActionUp(float)}.
  */
 private float hideWeight = 0.25f;
 //3.注意,这个接口用来设置「需要自定义自动到达顶部/隐藏的阈值」
 public void setHideWeight(float hideWeight) {
   if (hideWeight <= 0 || hideWeight > 1)
     throw new IllegalArgumentException("hideWeight should belong (0f,1f]");
   this.hideWeight = hideWeight;
 }
 private Scroller mScroller;
 /**
  * It means the {@link #childView} is arriving the top of parent or else.
  */
 private boolean arriveTop = false;
 /**
  * the {@link #childView} Initially visible height
  */
 private float visibilityHeight;
 //1.初始化Handle显示高度,建议您在xml中设置对应属性来实现该效果
 public void setVisibilityHeight(float visibilityHeight) {
   this.visibilityHeight = visibilityHeight;
 }
 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);
 }
 /**
  * Get the config from {@link R.styleable}, then init other configrations{@link #initConfig(Context)}.
  *
  * @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);
 }
 private void initConfig(Context context) {
   if (mScroller == null)
     mScroller = new Scroller(context);
   this.setBackgroundColor(Color.TRANSPARENT);
 }
 /**
  * It start a judgement for ensure the child-view be unique in this method,then assgin it
  * to {{@link #childView}.
  * this method will be called before the {@link #onMeasure(int, int)}
  */
 @Override
 protected void onFinishInflate() {
   super.onFinishInflate();
   if (getChildCount() == 0 || getChildAt(0) == null) {
     throw new RuntimeException("there have no child-View in the SlideBottomLayout!");
   }
   if (getChildCount() > 1) {
     throw new RuntimeException("there just alow one child-View in the SlideBottomLayout!");
   }
   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 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);
 }
 @Override
 public void computeScroll() {
   super.computeScroll();
   if (mScroller == null)
     mScroller = new Scroller(getContext());
   if (mScroller.computeScrollOffset()) {
     scrollTo(0, mScroller.getCurrY());
     postInvalidate();
   }
 }
 /**
  * When the touch event is {@link MotionEvent#ACTION_UP},
  * then judge the state of view group and control the {@link Scroller} to scroll.
  * <p>
  * In this ViewGroup, we set the rules that is if this scroll gesture's move distance
  * more than {@link #movedMaxDis} * {@link #hideWeight}(default hideWeight value is 1/4 heights
  * of this ViewGroup), then call {@link #hide()} or {@link #show()} right now. which method will
  * be call depends on {@link #arriveTop}.
  * <p
  * if the scroll gesture's move distance don't reach the goal value, then call the
  * {@link ShortSlideListener#onShortSlide(float)} if you call {@link #setShortSlideListener(ShortSlideListener)}
  * init this ViewGroup. else will call {@link #hide()}.
  *
  * @param eventY The location of trigger
  * @return Be used to determine consume this event or else.
  */
 public boolean touchActionUp(float eventY) {
   if (movedDis > movedMaxDis * hideWeight) {
     switchVisible();
   } else {
     if (shortSlideListener != null) {
       shortSlideListener.onShortSlide(eventY);
     } else {
       hide();
     }
   }
   return true;
 }
 /**
  * When the touch event is {@link MotionEvent#ACTION_MOVE},
  * then judge the state of view group and control the {@link Scroller} to scroll.
  * <p>
  * In this ViewGroup, we set the rules that is if this scroll gesture's move distance
  * more than {@link #movedMaxDis} * {@link #hideWeight}(default hideWeight value is 1/4 heights of this ViewGroup),
  * then call {@link #hide()} or {@link #show()} right now.
  * <p>
  *
  * @param eventY The location of trigger
  * @return Be used to determine consume this event or else.
  */
 public boolean touchActionMove(float eventY) {
   moveY = (int) eventY;
   //the dy is sum of the move distance, the value > 0 means scroll up, the value < 0 means scroll down.
   final int dy = downY - moveY;
   if (dy > 0) {        //scroll up
     movedDis += dy;
     if (movedDis > movedMaxDis)
       movedDis = movedMaxDis;
     if (movedDis < movedMaxDis) {
       scrollBy(0, dy);
       downY = moveY;
       return true;
     }
   } else {        //scroll down
     movedDis += dy;
     if (movedDis < 0) movedDis = 0;
     if (movedDis > 0) {
       scrollBy(0, dy);
     }
     downY = moveY;
     return true;
   }
   return false;
 }
 /**
  * When the touch event is {@link MotionEvent#ACTION_DOWN},
  * Record the location of this action.
  *
  * @param eventY The location of trigger
  * @return Be used to determine consume this event or else.
  */
 public boolean touchActionDown(float eventY) {
   downY = (int) eventY;
   //Whether custom this gesture.
   if (!arriveTop && downY < movedMaxDis) {
     return false;
   } else
     return true;
 }
 /**
  * the extand method for showing {@link SlideBottomLayout}
  */
 public void show() {
   scroll2TopImmediate();
 }
 /**
  * the extand method for hiding {@link SlideBottomLayout}
  */
 public void hide() {
   scroll2BottomImmediate();
 }
 /**
  * @return The ViewGroup is arrive top or else.
  */
 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;
 }
 public void scroll2BottomImmediate() {
   mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
   postInvalidate();
   movedDis = 0;
   arriveTop = false;
 }
}

注释也比较明了,如果有疑问,详细请参照SlideBottomLayout-Android 简单易上手的Android底部上拉控件

里面有相对详细的使用说明,此外,如果还有一些需求,您可以在issue中提出,提前感谢!

来源:http://blog.csdn.net/mq2553299/article/details/77776967

标签:Android,自定义,底部,上拉,控件
0
投稿

猜你喜欢

  • eclipse下配置Spring环境的方法步骤

    2023-02-27 20:39:26
  • C++ 让函数返回数组的方法

    2022-12-04 06:49:33
  • Java并发的CAS原理与ABA问题的讲解

    2023-11-25 12:17:21
  • java调用淘宝api联网查询ip归属地

    2022-06-11 12:21:20
  • SpringBoot 项目的创建与启动步骤详解

    2022-01-23 16:51:17
  • Spring Boot热加载jar实现动态插件的思路

    2021-09-25 16:37:55
  • Java CAS原子操作详解

    2023-05-06 15:37:15
  • java客户端登陆服务器用户名验证

    2023-11-09 07:03:09
  • Java设计模式之GOF23全面讲解

    2023-06-24 06:15:06
  • springboot mybatis里localdatetime序列化问题的解决

    2023-06-25 06:58:18
  • Unity3D实现物体排成弧行

    2022-10-15 12:23:06
  • 全面解析Android中对EditText输入实现监听的方法

    2022-09-15 15:05:55
  • Java查看和修改线程优先级操作详解

    2023-09-13 08:25:30
  • android连接wifi时获取广播地址代码

    2022-11-20 13:08:32
  • Android实现短信验证码获取自动填写功能(详细版)

    2022-07-22 07:08:10
  • Android自定义顶部标题栏

    2023-02-21 19:03:47
  • Java中Servlet的生命周期详解

    2023-09-10 03:57:59
  • Java守护线程实例详解_动力节点Java学院整理

    2023-03-29 08:14:30
  • 解析Java中未被捕获的异常以及try语句的嵌套使用

    2022-10-18 20:03:48
  • springboot2.5.6集成RabbitMq实现Topic主题模式(推荐)

    2021-10-03 22:01:22
  • asp之家 软件编程 m.aspxhome.com