Android下拉阻尼效果实现原理及简单实例

前言

本文将通过代码讲解下拉阻尼效果的实现原理。

实现灵感来源于这篇博客,但是这篇博客的代码并不能让我满意,或者说是糟糕的,不过还是非常感谢作者带给我的启发。

现在大部分资讯类安卓APP都有一个下拉刷新的功能,又如微信联系人列表顶部的小程序入口,也使用了这种下拉阻尼的效果。

我的代码主要是解释其实现原理,为方便读者理解,所以代码逻辑非常简单,但如果想要实现例如下拉刷新转动的进度圈,还需要修改代码中的MoveHeaderTask类中的onProgressUpdate方法;如果要实现滑动列表顶部加入这种下拉阻尼效果,则需要修改代码中的onTouch方法,通过判断是否到达列表顶部来决定是否触发下拉阻尼效果的逻辑代码。

最新的微信版本还实现了一个具有惯性的滑动列表(不清楚这样表述是否正确),滑动的速度大小和小程序入口的下拉阻尼效果会形成互动,但这已不是本文讨论的重点,这需要感兴趣的读者自行对我的代码进行迭代。

运行效果如下:

如图,拉动"可见主体"到达一定高度,"隐藏头部"就会弹出,反之,向上滑动到一定高度,"隐藏头部"则会收回,如果未到达指定高度,则恢复原状。

实际运行效果其实很流畅,也不会出现上图中,头部无法完全隐藏的情况,只是AS自带的录屏工具比较差劲。我不建议把这个自定义控件用在对话框类型的activity上,因为前一个activity处于可见状态,可能会占用大量算力,导致动画效果不流畅,亲测。

原理

这种效果是通过自定义控件的方式来实现的,我自定义了一个控件类型,这个自定义控件(PullDownDumperLayout)继承自线性布局(LinearLayout)

用户可以下拉弹出的那个视图,例如微信的小程序列表,开发者只是将这个视图移出了父元素之外,所以不可见,我们暂且称之为隐藏头部,只有下拉到一定程度才会弹出,而主体,例如微信的联系人列表,则是可见的,布局见下图。

实现这个效果需要我们做三件工作:

1.隐藏作为头部的控件
2.监听用户对屏幕的操作事件
3.实现下拉回弹的动画效果

我们这个自定义控件会自动获取内部第一个子元素充当头部,其余的元素则是充当可见的主体(详见代码中的注释)。

基本的布局原理差不多就这样了,但是我们还需要让自定义控件监听用户的手势操作,例如上下滑动等。这里我和灵感来源的那篇博客一样,让自定义控件实现View.OnTouchListener接口,实现内部的onTouch方法可以监听来自屏幕的所有触摸操作。代码中我让头部和第二个子元素(可见的主体)注册了这个监听器,这是为了方便读者理解,读者可根据自己的需求进行修改。

注意,对于不能监听屏幕触摸事件的控件需要添加:

android:clickable="true"

至此,我们已经可以进行布局和监听用户手势了,但是还需要实现一个头部展开和隐藏的动画效果。当用户将隐藏头部下拉或上滑到一定高度时,这个效果就会被触发,这需要依赖上面所述的onTouch方法。动画效果的实现需要另开一个线程进行操作,线程的启动方式我们可以采用继承AsyncTask类来实现。

除此之外,我们可能会多次复用这个控件,所以在自定义控件类的最后还需要一些调整参数的set方法。

这里提个醒,在接下来的代码中,我们的自定义控件因为继承自LinearLayout,里面需要重写onLayout方法,而onLayout方法顾名思义就是布局,这个方法在Activity中的onCreate方法执行之后才会被调用,所以我们可以在ActivityonCreate方法中利用findViewById获取实例,调用上面提到的set方法进行参数的初始化。

LinearLayout中不止onLayout一个方法,详细解析请读者移步其他关于XML标签加载过程的文章,这里不做赘述。

代码

PullDownDumperLayout .java:

public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {

  /**
   * 取布局中的第一个子元素为下拉隐藏头部
   */
  private View mHeadLayout;

  /**
   * 隐藏头部布局的高的负值
   */
  private int mHeadLayoutHeight;

  /**
   * 隐藏头部的布局参数
   */
  private MarginLayoutParams mHeadLayoutParams;

  /**
   * 判断是否为第一次初始化,第一次初始化需要把headView移出界面外
   */
  private boolean mOnLayoutIsInit=false;

  /**
   * 移动时,前一个坐标
   */
  private float mMoveY;

  /**
   * 如果为false,会退出头部展开或隐藏动画
   */
  private boolean mChangeHeadLayoutTopMargin;

  /**
   * 触发动画的分界线,由mRatio计算得到
   */
  private int mBoundary;

  /**
   * 头部布局的隐藏和展开速度,以及单次执行时间
   */
  private int mHeadLayoutHideSpeed;
  private int mHeadLayoutUnfoldSpeed;
  private long mSleepTime;

  /**
   * 触发动画的分界线,头部布局上半部分和整体高度的比例
   */
  private double mRatio;

  public PullDownDumperLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    //初始化参数,根据自己的需求调整
    mHeadLayoutHideSpeed=-20;
    mHeadLayoutUnfoldSpeed=20;
    mSleepTime=10;
    mRatio=0.5;
  }

  /**
   * 布局开始设置每一个控件
   * 在activity的onCreate执行之后才会执行
   * 因此可以在onCreate中调用set方法设置参数
   */
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    if(!mOnLayoutIsInit && changed) {
      //将第一个子元素作为头部移出界面外
      mHeadLayout = this.getChildAt(0);
      mHeadLayoutHeight=-mHeadLayout.getHeight();
      mBoundary=(int)(mRatio*mHeadLayoutHeight);//计算触发动画分界线
      mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();
      mHeadLayoutParams.topMargin=mHeadLayoutHeight;
      mHeadLayout.setLayoutParams(mHeadLayoutParams);
      //TODO 设置手势监听器,不能触碰的控件需要添加android:clickable="true"
      getChildAt(1).setOnTouchListener(this);
      mHeadLayout.setOnTouchListener(this);
      //标记已被初始化
      mOnLayoutIsInit=true;
    }
  }

  /**
   * 屏幕触摸操作监听器
   * @return false则注册本监听器的控件将不会对事件做出响应,true则相反
   */
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        mMoveY=event.getRawY();//捕获按下时的坐标,初始化mMoveY
        mChangeHeadLayoutTopMargin=false;
        break;
      case MotionEvent.ACTION_MOVE:
        float currY=event.getRawY();
        int vector=(int)(currY-mMoveY);//向量,用于判断手势的上滑和下滑
        mMoveY=currY;
        //判断是否为滑动
        if(Math.abs(vector)==0){
          return false;
        }
        //头部完全隐藏时不再向上滑动
        if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {
          return false;
        }
        //头部完全展开时不再向下滑动
        if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {
          return false;
        }

        //对增量进行修正,对滑动距离进行减半
        int topMargin = mHeadLayoutParams.topMargin + (vector/2);//阻尼值
        if(topMargin>0){
          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式
          // 如需平滑过渡,要另开线程,并且监听到ACTION_DOWN时线程可被打断
          topMargin = 0;
        }
        else if(topMargin<mHeadLayoutHeight){
          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式
          // 如需平滑过渡,要另开线程,并且监听ACTION_DOWN时线程可被打断
          topMargin = mHeadLayoutHeight;
        }
        //用户对屏幕的滑动将会改变控件的TopMargin
        mHeadLayoutParams.topMargin = topMargin ;
        mHeadLayout.setLayoutParams(mHeadLayoutParams);
        break;
      default:
        //TODO 出现其他触碰事件,如MotionEvent.ACTION_UP时,根据阈值判断此时头部应该弹出还是隐藏
        mChangeHeadLayoutTopMargin=true;
        if(mHeadLayoutParams.topMargin<=mBoundary){
          //隐藏
          new MoveHeaderTask().execute(true);
        }
        else{
          //展开
          new MoveHeaderTask().execute(false);
        }
        break;
    }
    return false;
  }

  /**
   * 新线程,隐藏或者展开头部布局,线程可被ACTION_DOWN打断
   */
  class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {

    /**
     *
     * @param opt true为隐藏动画,false为展开动画
     * @return
     */
    @Override
    protected Integer doInBackground(Boolean... opt) {
      int topMargin=mHeadLayoutParams.topMargin;
      //true为隐藏,false为展开
      int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;
      while(mChangeHeadLayoutTopMargin){
        topMargin += speed;
        if (topMargin <= mHeadLayoutHeight||topMargin>=0) {
          topMargin=(opt[0])?mHeadLayoutHeight:0;
          publishProgress(topMargin);
          break;
        }
        publishProgress(topMargin);
        sleep(mSleepTime);
      }
      return null;
    }

    //调用publishProgress后会执行
    @Override
    protected void onProgressUpdate(Integer... topMargin) {
      mHeadLayoutParams.topMargin=topMargin[0];
      mHeadLayout.setLayoutParams(mHeadLayoutParams);
    }

  }

  //调整参数
  public void setHeadLayoutHideSpeed(int speed){
    this.mHeadLayoutHideSpeed=speed;
  }
  public void setHeadLayoutUnfoldSpeed(int speed){
    this.mHeadLayoutUnfoldSpeed=speed;
  }
  public void setSleepTime(long time){
    this.mSleepTime=time;
  }
  public void setRatio(double ratio){
    this.mRatio=ratio;
  }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <com.example.pulldowndumpertest.PullDownDumperLayout
    android:tag="记得将这个标签修改为自己的包名"
    android:id="@+id/PullDownDumper"
    android:layout_width="900px"
    android:layout_height="1920px"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:background="@null"
    android:orientation="vertical">
    <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="500px"
      android:orientation="vertical"
      android:background="@color/colorPrimary"
      android:clickable="true">
      <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="隐藏头部"
        android:textSize="100px"
        android:gravity="center"
        android:textColor="#FFFFFF"
        android:background="@null"/>
    </LinearLayout>
    <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="1700px"
      android:background="@color/colorPrimaryDark"
      android:clickable="true">
      <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="可见主体"
        android:textSize="100px"
        android:gravity="center"
        android:textColor="#FFFFFF"
        android:background="@null"/>
    </LinearLayout>
  </com.example.pulldowndumpertest.PullDownDumperLayout>

</android.support.constraint.ConstraintLayout>

MainActivity.java:

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //TODO 读者可在这里初始化参数
    PullDownDumperLayout pddl=findViewById(R.id.PullDownDumper);

  }
}

下面是笔者正在使用的自定义控件,比上述的控件多了一个效果:

头部处于隐藏或展开的不同状态时,触发动画效果的分界线可以随状态不同而改变。

还是拿最新版的微信小程序入口来讲,用户在下拉时,小程序界面会占用整个屏幕,如果触发动画的分界线太低,这样导致的结果是用户可能无法通过上滑重新返回联系人列表,但由于微信没有对滑动距离进行减半处理,所以不存在上述问题,可能是出于防止误触的原因,从小程序界面返回联系人列表的方式改用点击底部的一个按钮。而我的控件可以通过改变触发动画效果的分界线来解决这一问题,感兴趣的读者可以研究一下。

public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {

  /**
   * 取布局中的第一个子元素为下拉隐藏头部
   */
  private View mHeadLayout;

  /**
   * 隐藏头部布局的高的负值
   */
  private int mHeadLayoutHeight;

  /**
   * 隐藏头部的布局参数
   */
  private MarginLayoutParams mHeadLayoutParams;

  /**
   * 判断是否为第一次初始化,第一次初始化需要把headView移出界面外
   */
  private boolean mOnLayoutIsInit=false;

  /**
   * 从配置获取的滚动判断阈值,为两点间的距离,超过此阈值判断为滚动
   */
//  private int mScaledTouchSlop;

  /**
   * 按下时的y轴坐标
   */
//  private float mDownY;

  /**
   * 移动时,前一个坐标
   */
  private float mMoveY;

  /**
   * 如果为false,会退出头部展开或隐藏动画
   */
  private boolean mChangeHeadLayoutTopMargin;

  /**
   * 头部布局的隐藏和展开速度,以及单次执行时间
   */
  private int mHeadLayoutHideSpeed;
  private int mHeadLayoutUnfoldSpeed;
  private long mSleepTime;

  /**
   * 初始化头部布局的偏移值,数值越大,头部可见部分越多,预设值为0,即初始时头部完全不可见
   */
  private int mTopMarginOffset;

  /**
   * 触发动画的分界线,头部布局上半部分和整体高度的比例
   */
  private double mUnfoldRatio;
  private double mHideRatio;

  /**
   * 触发动画的分界线,初始值由mRatio计算得到
   * 头部处于隐藏时等于mUnfoldBoundary
   * 头部处于展开时等于mHideBoundary
   * mBoundary在onTouch的ACTION_DOWN中变化
   */
  private int mBoundary;
  private int mUnfoldBoundary;
  private int mHideBoundary;

  /**
   * 阻尼值,越大越难拖动,呈线性趋势
   */
  private int mDumper;

  public PullDownDumperLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
//    mScaledTouchSlop= ViewConfiguration.get(context).getScaledTouchSlop();
    mHeadLayoutHideSpeed=-30;
    mHeadLayoutUnfoldSpeed=30;
    mSleepTime=10;
    mUnfoldRatio=0.6;
    mHideRatio=mUnfoldRatio;
    mDumper=2;
    mTopMarginOffset=-200;
  }

  /**
   * 布局开始设置每一个控件
   * 在activity的onCreate执行之后才会执行
   * 因此可以在onCreate中调用set方法设置参数
   */
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    //只初始化一次
    if(!mOnLayoutIsInit && changed) {
      //将第一个子元素作为头部移出界面外
      mHeadLayout = this.getChildAt(0);
      mHeadLayoutHeight=-mHeadLayout.getHeight();
      mUnfoldBoundary=(int)(mUnfoldRatio*mHeadLayoutHeight);//计算触发展开动画分界线
      mHideBoundary=(int)(mHideRatio*mHeadLayoutHeight);//计算触发隐藏动画分界线
      mBoundary=mUnfoldBoundary;//触发动画的分界线初始为mUnfoldBoundary
      mHeadLayoutHeight-=mTopMarginOffset;//头部隐藏布局可见的部分
      mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();
      mHeadLayoutParams.topMargin=mHeadLayoutHeight;
      mHeadLayout.setLayoutParams(mHeadLayoutParams);
      //TODO 设置手势监听器,不能触碰的控件需要添加android:clickable="true"
      getChildAt(1).setOnTouchListener(this);
      mHeadLayout.setOnTouchListener(this);
      //标记已被初始化
      mOnLayoutIsInit=true;
    }
  }

  /**
   * 屏幕触摸操作监听器
   * @return false: 注册本监听器的控件将不会对事件做出响应,true则相反
   */
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        //根据此时处于完全展开或完全隐藏决定mBoundary的值,如果两种情况都不满足则不做改变
        if(mHeadLayoutParams.topMargin==mHeadLayoutHeight)
          mBoundary=mUnfoldBoundary;
        else if(mHeadLayoutParams.topMargin==0)
          mBoundary=mHideBoundary;

//        mDownY=event.getRawY();//获取按下的屏幕y坐标
        mMoveY=event.getRawY();
        mChangeHeadLayoutTopMargin=false;//false会打断隐藏或展开头部布局的动画
        break;
      case MotionEvent.ACTION_MOVE:
        float currY=event.getRawY();
        int vector=(int)(currY-mMoveY);//向量,用于判断手势的上滑和下滑
        mMoveY=currY;
        //判断是否为滑动
        if(Math.abs(vector)==0){
          return false;
        }
        //头部完全隐藏时不再向上滑动
        if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {
          return false;
        }
        //头部完全展开时不再向下滑动
        else if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {
          return false;
        }

        //对增量进行修正
        int topMargin = mHeadLayoutParams.topMargin + (vector/mDumper);
        if(topMargin>0){
          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式
          // 如需实现平滑过渡,要另开线程,并且监听到ACTION_DOWN时线程可被打断
          topMargin = 0;
        }
        else if(topMargin<mHeadLayoutHeight){
          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式
          // 如需实现平滑过渡,要另开线程,并且监听ACTION_DOWN时线程可被打断
          topMargin = mHeadLayoutHeight;
        }

        //使参数生效
        mHeadLayoutParams.topMargin = topMargin ;
        mHeadLayout.setLayoutParams(mHeadLayoutParams);
        break;
      default:
        //出现其他触碰事件,如MotionEvent.ACTION_UP时,根据阈值mBoundary判断此时头部应该弹出还是隐藏
        mChangeHeadLayoutTopMargin=true;//允许执行动画
        if(mHeadLayoutParams.topMargin<=mBoundary){
          //隐藏
          new MoveHeaderTask().execute(true);
        }
        else{
          //展开
          new MoveHeaderTask().execute(false);
        }
        break;
    }
    return false;
  }

  /**
   * 新线程,隐藏或者展开头部布局,线程可被ACTION_DOWN打断
   */
  private class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {

    /**
     *
     * @param opt true为隐藏动画,false为展开动画
     * @return
     */
    @Override
    protected Integer doInBackground(Boolean... opt) {
      int topMargin=mHeadLayoutParams.topMargin;
      //true为隐藏,false为展开
      int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;
      while(mChangeHeadLayoutTopMargin){
        topMargin += speed;
        if (topMargin <= mHeadLayoutHeight||topMargin>=0) {
          topMargin=(opt[0])?mHeadLayoutHeight:0;
          publishProgress(topMargin);
          break;
        }
        publishProgress(topMargin);
        sleep(mSleepTime);
      }
      return null;
    }

    //调用publishProgress后会执行
    @Override
    protected void onProgressUpdate(Integer... topMargin) {
      mHeadLayoutParams.topMargin=topMargin[0];
      mHeadLayout.setLayoutParams(mHeadLayoutParams);
    }

  }

  //调整参数
  public void setHeadLayoutHideSpeed(int speed){
    this.mHeadLayoutHideSpeed=speed;
  }
  public void setHeadLayoutUnfoldSpeed(int speed){
    this.mHeadLayoutUnfoldSpeed=speed;
  }
  public void setSleepTime(long time){
    this.mSleepTime=time;
  }
  public void setDumper(int dumper){
    this.mDumper=dumper;
  }
  public void setTopMarginOffset(int offset){
    this.mTopMarginOffset=-offset;
  }

  /**
   * 头部处于隐藏状态时,触发展开动画的分界线
   * @param ratio 头部布局上部分与下部分的分界线
   */
  public void setUnfoldRatio(double ratio){
    this.mUnfoldRatio=ratio;
  }

  /**
   * 头部处于展开状态时,触发隐藏动画的分界线
   * @param ratio 头部布局上部分与下部分的分界线
   */
  public void setHideRatio(double ratio){
    this.mHideRatio=ratio;
  }
}```

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

(0)

相关推荐

  • Android PullToRefreshLayout下拉刷新控件的终结者

    说到下拉刷新控件,网上版本有很多,很多软件也都有下拉刷新功能.有一个叫XListView的,我看别人用过,没看过是咋实现的,看这名字估计是继承自ListView修改的,不过效果看起来挺丑的,也没什么扩展性,太单调了.看了QQ2014的列表下拉刷新,发现挺好看的,我喜欢,贴一下图看一下qq的下拉刷新效果: 不错吧?嗯,是的.一看就知道实现方式不一样.咱们今天就来实现一个下拉刷新控件.由于有时候不仅仅是ListView需要下拉刷新,ExpandableListView和GridView也有这个需求,

  • Android下拉刷新上拉加载控件(适用于所有View)

    前面写过一篇关于下拉刷新控件的文章下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能.不仅如此,我已经把它改成了对所有View都通用!可以随心所欲使用这两个功能~~ 我做了一个大集合的demo,实现了ListView.GridView.ExpandableListView.ScrollView.WebView.ImageView.TextView的下拉刷新和上拉加载.后面会提供demo的

  • Android下拉刷新ListView——RTPullListView(demo)

    下拉刷新在越来越多的App中使用,已经形成一种默认的用户习惯,遇到列表显示的内容时,用户已经开始习惯性的拉拉.在交互习惯上已经形成定性.之前在我的文章<IOS学习笔记34-EGOTableViewPullRefresh实现下拉刷新>中介绍过如何在IOS上实现下拉刷新的功能.今天主要介绍下在Android上实现下拉刷新的Demo,下拉控件参考自Github上开源项目PullToRefresh,并做简单修改.最终效果如下:                         工程结构如下: 使用过程中

  • Android中Spinner(下拉框)控件的使用详解

    android给我们提供了一个spinner控件,这个控件主要就是一个列表,那么我们就来说说这个控件吧,这个控件在以前的也看见过,但今天还是从新介绍一遍吧. Spinner位于 android.widget包下,每次只显示用户选中的元素,当用户再次点击时,会弹出选择列表供用户选择,而选择列表中的元素同样来自适配器.Spinner是View类得一个子类. 1.效果图 2.创建页面文件(main.xml) <Spinner android:id="@+id/spinner1" and

  • Android属性动画实现布局的下拉展开效果

    在Android的3.0之后,google又提出了属性动画的这样一个框架,他可以更好的帮助我们实现更丰富的动画效果.所以为了跟上技术的步伐,今天就聊一聊属性动画. 这一次的需求是这样的:当点击一个View的时候,显示下面隐藏的一个View,要实现这个功能,需要将V iew的visibility属性设置gone为visible即可,但是这个过程是一瞬间的,并不能实现我们要的效果.所以,属性动画是个不错的方案. 先把效果贴上 第一个:  第二个: 前面的这个是隐藏着,后面这个是显示的.当点击这个箭头

  • Android实现三级联动下拉框 下拉列表spinner的实例代码

    主要实现办法:动态加载各级下拉值的适配器 在监听本级下拉框,当本级下拉框的选中值改变时,随之修改下级的适配器的绑定值              XML布局: 复制代码 代码如下: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_w

  • Android实现简单的下拉阻尼效应示例代码

    OS的下拉上拉都会出现一个很玄的动态效果.在Android中,虽然可以实现类似的效果,但有点不同的是,如果调用overScrollBy来实现类似的阻尼效应的话,最顶部会出现一片亮的区域,让人感觉不是很爽.所以决定不采用该方法来实现而是改用自定义的方式来实现. 下面是自定义控件的代码部分: public class MyView extends ScrollView { //记录下最开始点击的位置 int initY; //移动的位置 int deltaY; int touchY; //记录第一个

  • Android官方下拉刷新控件SwipeRefreshLayout使用详解

    可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介: SwipeRefreshLayout组件只接受一个子组件:即需要刷新的那个组件.它使用一个侦听机制来通知拥有该组件的监听器有刷新事件发生,换句话说我们的Activity必须实现通知的接口.该Activity负责处理事件刷新和刷新相应的视图.一旦监听者接收到该事件,就决定了刷新过程中应处理的地方.如果要展示一个"刷新动画",它必须

  • Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能(附源码)

    最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想.有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,十全十美的还真没找到.因此我也是放弃了在网上找现成代码的想法,自己花功夫编写了一种非常简单的下拉刷新实现方案,现在拿出来和大家分享一下.相信在阅读完本篇文章之后,大家都可以在自己的项目中一分钟引入下拉刷新功能. 首先讲一下实现原理.这里我们将采取的方案是使用组合View的方式,先自定义一个布局

  • Android中使用RecyclerView实现下拉刷新和上拉加载

    推荐阅读:使用RecyclerView添加Header和Footer的方法                       RecyclerView的使用之HelloWorld RecyclerView 是Android L版本中新添加的一个用来取代ListView的SDK,它的灵活性与可替代性比listview更好.本文给大家介绍如何为RecyclerView添加下拉刷新和上拉加载,过去在ListView当中添加下拉刷新和上拉加载是非常方便的利用addHeaderView和addFooterVie

随机推荐