Android制作简单垂直上拉下滑View效果

#技术教程 发布时间: 2026-01-18

一、简介

最近朋友公司需要实现一个垂直上拉下滑的View,该View最初只有一部分显示在屏幕最下方,上拉那一部分可以将该View全部拉出来并全部显示在屏幕上,下滑该View可以将该View隐藏在屏幕下。
先看一下最终实现效果吧。

二、实现思路

1、这个效果其实有很多实现方法,为了让松手时有一个viewpager一样的缓慢滑动的效果我选择用scrollBy配合Scroller,应该是既方便又实用的。
2、这个View的设计是这样的:
(1)将这个View的子view通过layout放在该View下面;
(2)通过重写onTouchEvent方法给这个子View滑动效果,在MOVE_UP的动作给这个子View加上Scroller平滑到View的顶部或者底部。
见图:

三、实现

1、先自定义一个属性,表示子View应该有多少部分露在外面,也就是上图中红色和绿色相交的部分。
在res文件夹-values文件夹下面创建一个attrs.xml文件

attrs.xml :

<resources>
 <declare-styleable name="MyScrollerView">
  <attr name="visibility_height" format="dimension"></attr>
 </declare-styleable>

</resources>

在XML文件中引用该属性:

<com.zw.myfirstapp.MyScrollerView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_alignParentBottom="true"
   android:background="@android:color/transparent"
   android:id="@+id/msv"
   app:visibility_height="100dp"
   ></com.zw.myfirstapp.MyScrollerView>

在代码中调用该属性(该View名字为MyScrollerView,我图方便继承的是LinearLayout,继承ViewGroup或者其他的布局View都可以):

public MyScrollerView(Context context) {
  this(context,null);
 }

 public MyScrollerView(Context context, AttributeSet attrs) {
  this(context, attrs,0);
 }

 public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);

  TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.MyScrollerView);
  visibilityHeight = ta.getDimension(R.styleable.MyScrollerView_visibility_height,200);
  ta.recycle();

  init(context);
 }

2、重写onFinishInflate方法,重写该方法的原因是我希望我只有一个子View,这样就好确定滑动的高度,不然我还需要重新计算子View们的高度总和,比较麻烦。这个方法会在onMeasure之前调用。

 @Override
 protected void onFinishInflate() {
  super.onFinishInflate();
  if(getChildCount() == 0 || getChildAt(0) == null){
   throw new RuntimeException("没有子控件!");
  }
  if(getChildCount() > 1){
   throw new RuntimeException("只能有一个子控件!");
  }
  mChild = getChildAt(0);
 }

3、init方法里做一些初始化操作,比如说创建一个Scroller对象,给View的背景设为透明:

 private void init(Context context) {
  mScroller = new Scroller(context);
  this.setBackgroundColor(Color.TRANSPARENT);
 }

4、重写onMeasure方法和onLayout方法,确定可以滑动的最大高度,以及子View的排列位置(其实可以不用重写onMeasure,我这样写只是习惯)。

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  mScrollHeight = (int) (mChild.getMeasuredHeight() - visibilityHeight);
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
  super.onLayout(changed, l, t, r, b);
  mChild.layout(0,mScrollHeight,mChild.getMeasuredWidth(),mChild.getMeasuredHeight() + mScrollHeight);
 }

5、先看我定义的成员变量的含义吧:

 /**
  * downY:手指按下时距离View顶部的距离
  * moveY:手指在屏幕上滑动的距离(不停变化)
  * movedY:手指在屏幕上总共滑动的距离(为了确定手指一共滑动了多少距离,不能超过可滑动的最大距离)
  */
 private int downY,moveY,movedY;

 //子View
 private View mChild;

 private Scroller mScroller;

 //可滑动的最大距离
 private int mScrollHeight;

 //子View是否在顶部
 private boolean isTop = false;

 //最初子View在View内可见的高度
 private float visibilityHeight;

6、重写onTouchEvent方法,做滑动判断,解释都写在注释里了:

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()){
   case MotionEvent.ACTION_DOWN:
    //手指按下时距离View上面的距离
    downY = (int) event.getY();

    //如果子View不在顶部 && 按下的位置在子View没有显示的位置,则不消费此次滑动事件,否则消费
    if(!isTop && downY < mScrollHeight ){
     return super.onTouchEvent(event);
    }
    return true;
   case MotionEvent.ACTION_MOVE:

    moveY = (int) event.getY();
    //deY是滑动的距离,向上滑时deY>0 ,向下滑时deY<0
    int deY = downY - moveY;

    //向上滑动时的处理
    if(deY > 0){
     //将每次滑动的距离相加,为了防止子View的滑动超过View的顶部
     movedY += deY;
     if(movedY > mScrollHeight) movedY = mScrollHeight;

     if(movedY < mScrollHeight){
      scrollBy(0,deY);
      downY = moveY;
      return true;
     }
    }

    //向下滑动时的处理,向下滑动时需要判断子View是否在顶部,如果不在顶部则不消费此次事件
    if(deY < 0 && isTop){
     movedY += deY;
     if(movedY < 0 ) movedY = 0;
     if(movedY > 0){
      scrollBy(0,deY);
     }
     downY = moveY;
     return true;
    }

    break;
   case MotionEvent.ACTION_UP:
    //手指抬起时的处理,如果向上滑动的距离超过了最大可滑动距离的1/4,并且子View不在顶部,就表示想把它拉上去
    if(movedY > mScrollHeight / 4 && !isTop){
     mScroller.startScroll(0,getScrollY(),0,(mScrollHeight - getScrollY()));
     invalidate();
     movedY = mScrollHeight;
     isTop = true;
    }else {
     //否则就表示放弃本次滑动,让它滑到最初的位置
     mScroller.startScroll(0,getScrollY(),0, -getScrollY());
     postInvalidate();
     movedY = 0;
     isTop = false;
    }

    break;
  }
  return super.onTouchEvent(event);
 }

7、最后要重写一个computeScroll方法,该方法是用来配合scroller的:

 @Override
 public void computeScroll() {
  super.computeScroll();
  if(mScroller.computeScrollOffset()){
   scrollTo(0,mScroller.getCurrY());
   postInvalidate();
  }
 }

8、关于scroller的用法,可参考郭霖的这篇博客:http://blog.csdn.net/guolin_blog/article/details/48719871

四、完整代码:

xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 android:id="@+id/activity_main"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

  <com.zw.myfirstapp.MyScrollerView
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_alignParentBottom="true"
   android:background="@android:color/transparent"
   android:id="@+id/msv"
   app:visibility_height="100dp"
   >
   <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:background="@mipmap/b"
    android:gravity="center"
    android:orientation="vertical">
    <Button
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:id="@+id/btn"
     android:text="我是一个按钮"/>
   </LinearLayout>

  </com.zw.myfirstapp.MyScrollerView>

</RelativeLayout>

MyScrollerView:

public class MyScrollerView extends LinearLayout {

 /**
  * downY:手指按下时距离View顶部的距离
  * moveY:手指在屏幕上滑动的距离(不停变化)
  * movedY:手指在屏幕上总共滑动的距离(为了确定手指一共滑动了多少距离,不能超过可滑动的最大距离)
  */
 private int downY,moveY,movedY;

 //子View
 private View mChild;

 private Scroller mScroller;

 //可滑动的最大距离
 private int mScrollHeight;

 //子View是否在顶部
 private boolean isTop = false;

 //最初子View在View内可见的高度
 private float visibilityHeight;

 public MyScrollerView(Context context) {
  this(context,null);
 }

 public MyScrollerView(Context context, AttributeSet attrs) {
  this(context, attrs,0);
 }

 public MyScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);

  TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.MyScrollerView);
  visibilityHeight = ta.getDimension(R.styleable.MyScrollerView_visibility_height,200);
  ta.recycle();

  init(context);
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  mScrollHeight = (int) (mChild.getMeasuredHeight() - visibilityHeight);
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
  super.onLayout(changed, l, t, r, b);
  mChild.layout(0,mScrollHeight,mChild.getMeasuredWidth(),mChild.getMeasuredHeight() + mScrollHeight);
 }

 private void init(Context context) {
  mScroller = new Scroller(context);
  this.setBackgroundColor(Color.TRANSPARENT);
 }

 @Override
 protected void onFinishInflate() {
  super.onFinishInflate();
  if(getChildCount() == 0 || getChildAt(0) == null){
   throw new RuntimeException("没有子控件!");
  }
  if(getChildCount() > 1){
   throw new RuntimeException("只能有一个子控件!");
  }
  mChild = getChildAt(0);
 }


 @Override
 public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()){
   case MotionEvent.ACTION_DOWN:
    //手指按下时距离View上面的距离
    downY = (int) event.getY();

    //如果子View不在顶部 && 按下的位置在子View没有显示的位置,则不消费此次滑动事件,否则消费
    if(!isTop && downY < mScrollHeight ){
     return super.onTouchEvent(event);
    }
    return true;
   case MotionEvent.ACTION_MOVE:

    moveY = (int) event.getY();
    //deY是滑动的距离,向上滑时deY>0 ,向下滑时deY<0
    int deY = downY - moveY;

    //向上滑动时的处理
    if(deY > 0){
     //将每次滑动的距离相加,为了防止子View的滑动超过View的顶部
     movedY += deY;
     if(movedY > mScrollHeight) movedY = mScrollHeight;

     if(movedY < mScrollHeight){
      scrollBy(0,deY);
      downY = moveY;
      return true;
     }
    }

    //向下滑动时的处理,向下滑动时需要判断子View是否在顶部,如果不在顶部则不消费此次事件
    if(deY < 0 && isTop){
     movedY += deY;
     if(movedY < 0 ) movedY = 0;
     if(movedY > 0){
      scrollBy(0,deY);
     }
     downY = moveY;
     return true;
    }

    break;
   case MotionEvent.ACTION_UP:
    //手指抬起时的处理,如果向上滑动的距离超过了最大可滑动距离的1/4,并且子View不在顶部,就表示想把它拉上去
    if(movedY > mScrollHeight / 4 && !isTop){
     mScroller.startScroll(0,getScrollY(),0,(mScrollHeight - getScrollY()));
     invalidate();
     movedY = mScrollHeight;
     isTop = true;
    }else {
     //否则就表示放弃本次滑动,让它滑到最初的位置
     mScroller.startScroll(0,getScrollY(),0, -getScrollY());
     postInvalidate();
     movedY = 0;
     isTop = false;
    }

    break;
  }
  return super.onTouchEvent(event);
 }

 @Override
 public void computeScroll() {
  super.computeScroll();
  if(mScroller.computeScrollOffset()){
   scrollTo(0,mScroller.getCurrY());
   postInvalidate();
  }
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。




上一篇 : ng-options和ng-checked在表单中的高级运用(推荐)

下一篇 : android自定义view制作圆形进度条效果

推荐阅读

电话:400 76543 55
邮箱:915688610@qq.com
品牌营销
客服微信
搜索营销
公众号
©  丽景创新 版权所有 赣ICP备2024032158号 
宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 宜昌市隼壹珍商贸有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 内江振祥营销策划有限公司 恩施州毯滚百货有限公司 恩施州毯滚百货有限公司 襄阳市蜂欢商贸有限公司 襄阳市蜂欢商贸有限公司 恩施州换冯百货有限公司 恩施州换冯百货有限公司 恩施州健提百货有限公司 恩施州健提百货有限公司 西安益零商贸有限公司 西安益零商贸有限公司 南奥教育 南奥教育 南奥教育 南奥教育 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南奥教育网 南奥教育网 南奥教育网 南奥教育网 南奥学习网 南奥学习网 南奥学习网 南奥学习网 南奥教育 南奥教育 南奥留学记 南奥留学记 南奥教育 南奥教育 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌市南奥教育咨询有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 南昌壹佳企网络通信有限公司 广照天下广告 广照天下广告 广照天下广告策划 广照天下广告策划 广照天下 广照天下 广照天下 广照天下 广照天下 广照天下 广照天下广告策划 广照天下广告策划 广照天下广告策划 广照天下广告策划 南昌市广照天下广告策划有限公司 南昌市广照天下广告策划有限公司 南昌市广照天下广告策划有限公司 南昌市广照天下广告策划有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 宿州市腾雀网络科技有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司 九江市云仁商务咨询有限公司
品牌营销
专业SEO优化
添加左侧专家微信
获取产品详细报价方案