ListView实现QQ空间和微信朋友圈头部刷新效果

ListView实现QQ空间和微信朋友圈头部刷新效果

先上图
QQ空间效果
朋友圈效果
Demo地址

自定义ListView

如何实现?先上代码,思考一下,然后再讲解实现步骤。

定义刷新接口

public interface IRefreshHeader {

  int STATE_NORMAL = 0;
  int STATE_REFRESHING = 1;
  int STATE_DONE = 2;

  /**
   * 正在刷新
   */
  void onRefreshing();

  /**
   * 下拉移动
   */
  void onMove(float offSet);

  /**
   * 下拉松开
   */
  boolean onRelease();

  /**
   * 下拉刷新完成
   */
  void refreshComplete();

  /**
   * 获取HeaderView
   */
  View getHeaderView();

  /**
   * 获取Header拉伸的长度
   */
  int getOffset();
}

然后是自定义ListView中的关键代码:

/**
   * 重写滑动过度回调方法
   *
   * @param deltaY 正数是上拉 负数是下拉
   */
  @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX,
      int maxOverScrollY, boolean isTouchEvent) {
    if (deltaY < 0 && isTouchEvent) { //下拉 ImageView进行放大效果
      mRefreshHeader.onMove(deltaY);
    } else if (deltaY > 0 && isTouchEvent) { //上拉过度(ListView内容高度不足一屏) 减少ImageView大小
      mRefreshHeader.onMove(deltaY);
    }
    return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
  }

  /**
   * 下拉后往上推 需要ImageView变小(ListView内容高度大于一个屏幕)
   */
  @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    int top = mRefreshHeader.getHeaderView().getTop();//header 滑动到屏幕上方的距离
    if (top < 0) {
      mRefreshHeader.onMove(-top);
    }
    super.onScrollChanged(l, t, oldl, oldt);
  }

  /**
   * 释放手指后判断是否刷新
   */
  @Override public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_UP) {
      if (mRefreshHeader != null && mRefreshHeader.onRelease()) {
        if (mRefreshListener != null) {
          mRefreshing = true;
          mRefreshListener.onRefresh();
        }
      }
    }
    return super.onTouchEvent(ev);
  }

重点在这里

实现步骤:
1. 给ListView添加头部视图(addHeaderView)。
2. 头部视图主要由一个头图(android:scaleType="centerCrop"用来缩放)和提示刷新状态的图片构成。
3. 重写滑动过度回调方法overScrollBy判断下拉过度时头图的布局高度增加,然后头图请求重新布局(requestLayout()),实现头图拉伸。
4. 头部拉伸后上推,如果此时ListView的高度超过屏幕则通过onScrollChanged判断滑动距离(Header上部getTop()相对ListView父容器的距离,为负数表示向上滑动的距离)。
5. 头部拉伸后上推,如果此时ListView的高度不够一屏,则ListView无法滑动,不能通过onScrollChanged判断,这时overScrollBy又派上用场,判断是否上拉过度时头图布局减少,然后请求重新布局,减少头图高度,实现头图高度缩小。
6. 随着头部的拉伸和收缩实现刷新状态图片的位置变化和旋转,旋转角度和方向跟随头图每次拉伸和收缩的增量(负数下拉,正数上推),限制图片跟随头图下拉的高度。

通过上面的步骤实现ListView的头图高度的缩放,由于头图缩放模式为centerCrop则图片会根据ImageView的尺寸按中心点进行缩放,实现QQ空间的效果。
如果直接控制头部的高度而不是头图的高度就可以实现朋友圈刷新效果,具体见下面的代码实现。

QQ空间头部刷新效果实现

上代码

首先是QQ空间的头部布局,qzone_header.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

  <ImageView android:id="@+id/iv_header" android:layout_width="match_parent" android:layout_height="180dp" android:layout_marginBottom="40dp"
      android:scaleType="centerCrop" android:src="@drawable/img_header"/>

  <ImageView android:id="@+id/iv_refresh" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginStart="30dp"
      android:src="@drawable/refresh"/>

  <ImageView android:id="@+id/iv_icon" android:layout_width="80dp" android:layout_height="80dp" android:layout_gravity="end|bottom"
      android:layout_marginEnd="20dp" android:src="@drawable/img_avatar"/>

</FrameLayout>

注意头部布局中头图的缩放方式。

然后是QQ空间头部自定义View代码:

public class QzoneRefreshHeader extends FrameLayout implements IRefreshHeader {

  private ImageView mHeaderView; // 头图
  private int mHeaderViewHeight; // 头图高度
  private int mDeltaHeight; // 头图和头部布局的差值
  private ImageView mRefreshView; // 旋转刷新的图片
  private float mRefreshHideTranslationY; // 刷新图片上移的最大距离
  private float mRefreshShowTranslationY; // 刷新图片下拉的最大移动距离
  private float mRotateAngle; // 旋转角度
  private int mState = STATE_NORMAL;

  private WeakHandler mHandler = new WeakHandler();

  public QzoneRefreshHeader(Context context) {
    super(context);
    initView();
  }

  /**
   * @param context
   * @param attrs
   */
  public QzoneRefreshHeader(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
  }

  private void initView() {
    AbsListView.LayoutParams lp = new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    this.setLayoutParams(lp);
    this.setPadding(0, 0, 0, 0);
    inflate(getContext(), R.layout.qzone_header, this);
    mHeaderView = findViewById(R.id.iv_header);
    mRefreshView = findViewById(R.id.iv_refresh);
    measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    mHeaderViewHeight = mHeaderView.getMeasuredHeight();
    mDeltaHeight = getMeasuredHeight() - mHeaderViewHeight;
    mRefreshHideTranslationY = -mRefreshView.getMeasuredHeight() - 20;
    mRefreshShowTranslationY = mRefreshView.getMeasuredHeight();
  }

  private int getHeaderViewHeight() {
    return mHeaderView.getHeight();
  }

  private void setHeaderViewHeight(int height) {
    if (height < mHeaderViewHeight) height = mHeaderViewHeight;
    mHeaderView.getLayoutParams().height = height;
    mHeaderView.requestLayout();
  }

  public void setState(int state) {
    if (state == mState) return;

    if (state == STATE_REFRESHING) {  // 显示进度
      mRefreshView.setTranslationY(mRefreshShowTranslationY);
      refreshing();
    } else if (state == STATE_DONE) {
      reset();
    }

    mState = state;
  }

  @Override public void onRefreshing() {
    setState(STATE_REFRESHING);
  }

  @Override public void onMove(float offSet) {
    int top = getTop();// 相对父容器listview的顶部位置 负数表示向上划出父容器的距离
    int currentHeight = getHeaderViewHeight();
    int targetHeight = currentHeight - (int) offSet;
    if (offSet < 0 && top == 0) { // 向下拉
      setHeaderViewHeight(targetHeight);
      refreshTranslation(currentHeight, offSet); // 向上推
    } else if (offSet > 0 && currentHeight > mHeaderViewHeight) {
      layout(getLeft(), 0, getRight(), targetHeight + mDeltaHeight); //重新布局让header显示在顶端,直到不再缩小图片
      setHeaderViewHeight(targetHeight);
      refreshTranslation(currentHeight, offSet);
    }
  }

  /**
   * refreshView在刷新区间内相对位移并跟随位移速度旋转
   */
  private void refreshTranslation(int currentHeight, float offSet) {
    if ((currentHeight - mHeaderViewHeight) / 2 < mRefreshShowTranslationY - mRefreshHideTranslationY) { // 判断是否在非刷新区间
      float translationY = mRefreshView.getTranslationY() - offSet / 2; // 布局高度增加offset 相当于距离上边距offSet / 2
      if (translationY > mRefreshShowTranslationY) {
        translationY = mRefreshShowTranslationY;
      } else if (translationY < mRefreshHideTranslationY) {
        translationY = mRefreshHideTranslationY;
      }
      if (Math.abs(translationY) != mRefreshView.getTranslationY()) {
        mRefreshView.setTranslationY(translationY);
      }
    }
    mRefreshView.setRotation(mRotateAngle -= offSet);//旋转,角度大小跟随偏移量
  }

  @Override public boolean onRelease() {
    boolean isOnRefresh = false;
    int currentHeight = mHeaderView.getLayoutParams().height;// 使用 mHeaderView.getLayoutParams().height 可以防止快速快速下拉的时候图片不回弹
    if (currentHeight > mHeaderViewHeight) {
      if ((currentHeight - mHeaderViewHeight) / 2 > mRefreshShowTranslationY - mRefreshHideTranslationY && mState < STATE_REFRESHING) {
        setState(STATE_REFRESHING);
        isOnRefresh = true;
      }
      headerRest();
    }
    if (!isOnRefresh && mRefreshView.getTranslationY() != mRefreshHideTranslationY) {
      refreshRest();
    }
    return isOnRefresh;
  }

  @Override public void refreshComplete() {
    mHandler.postDelayed(new Runnable() {
      public void run() {
        setState(STATE_DONE);
      }
    }, 200);
  }

  @Override public View getHeaderView() {
    return this;
  }

  @Override public int getOffset() {
    return getHeight() - mHeaderViewHeight;
  }

  private void refreshing() {
    mHandler.postDelayed(new Runnable() {
      @Override public void run() {
        if (mState == STATE_REFRESHING) {
          mRefreshView.setRotation(mRotateAngle += 8);
          mHandler.post(this);
        }
      }
    }, 50);
  }

  public void reset() {
    refreshRest();
    mHandler.postDelayed(new Runnable() {
      public void run() {
        setState(STATE_NORMAL);
      }
    }, 500);
  }

  private void headerRest() {
    ValueAnimator animator = ValueAnimator.ofInt(mHeaderView.getLayoutParams().height, mHeaderViewHeight);
    //animator.setStartDelay(60);
    animator.setDuration(300).start();
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        if (mHeaderView.getLayoutParams().height == mHeaderViewHeight) { // 停止动画,防止快速上划松手后动画产生抖动
          animation.cancel();
        } else {
          setHeaderViewHeight((Integer) animation.getAnimatedValue());
        }
      }
    });
  }

  private void refreshRest() {
    ValueAnimator animator = ValueAnimator.ofFloat(mRefreshView.getTranslationY(), mRefreshHideTranslationY);
    animator.setStartDelay(60);
    animator.setDuration(300).start();
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        if (mRefreshView.getTranslationY() == mRefreshHideTranslationY) {
          animation.cancel();
        } else {
          mRefreshView.setTranslationY((Float) animation.getAnimatedValue());
        }
      }
    });
  }
}

微信朋友圈头部刷新效果实现

实现步骤和QQ空间效果实现类似,不同点主要在布局和操控的View,QQ空间效果主要操控头图的高度,而朋友圈效果主要操作Header本身的高度。贴代码看下区别:
首先是朋友圈头部布局moments_header.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clickable="false">

  <View android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="200dp" android:background="#ff000000"/>

  <ImageView android:id="@+id/iv_header" android:layout_width="match_parent" android:layout_height="300dp" android:layout_gravity="bottom"
      android:layout_marginTop="-100dp" android:layout_marginBottom="40dp" android:scaleType="centerCrop" android:src="@drawable/img_header1"/>

  <ImageView android:id="@+id/iv_refresh" android:layout_width="30dp" android:layout_height="30dp" android:layout_marginStart="30dp"
      android:src="@drawable/refresh"/>

  <ImageView android:id="@+id/iv_icon" android:layout_width="80dp" android:layout_height="80dp" android:layout_gravity="end|bottom"
      android:layout_marginEnd="20dp" android:src="@drawable/img_avatar1"/>

</FrameLayout>

注意:朋友圈这里让头图显示在布局外android:layout_marginTop="-100dp",头图大小不会在代码里改变。

朋友圈效果的Header代码:

public class MomentsRefreshHeader extends FrameLayout implements IRefreshHeader {

  private int mHeaderViewHeight; // 头部高度
  private ImageView mRefreshView; // 旋转刷新的图片
  private float mRefreshHideTranslationY; // 刷新图片上移的最大距离
  private float mRefreshShowTranslationY; // 刷新图片下拉的最大移动距离
  private float mRotateAngle; // 旋转角度
  private int mState = STATE_NORMAL;

  private WeakHandler mHandler = new WeakHandler();

  public MomentsRefreshHeader(Context context) {
    super(context);
    initView();
  }

  /**
   * @param context
   * @param attrs
   */
  public MomentsRefreshHeader(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
  }

  private void initView() {
    AbsListView.LayoutParams lp = new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    this.setLayoutParams(lp);
    this.setPadding(0, 0, 0, 0);
    inflate(getContext(), R.layout.moments_header, this);
    mRefreshView = findViewById(R.id.iv_refresh);
    measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    mHeaderViewHeight = getMeasuredHeight();
    mRefreshHideTranslationY = -mRefreshView.getMeasuredHeight() - 20;
    mRefreshShowTranslationY = mRefreshView.getMeasuredHeight();
  }

  private int getHeaderViewHeight() {
    return getHeight();// 这里和QQ空间不一样,使用自身高度
  }

  private void setHeaderViewHeight(int height) {
    if (height < mHeaderViewHeight) height = mHeaderViewHeight;
    getLayoutParams().height = height; // 这里和QQ空间不一样,修改自身高度
    requestLayout();
  }

  public void setState(int state) {
    if (state == mState) return;

    if (state == STATE_REFRESHING) {  // 显示进度
      mRefreshView.setTranslationY(mRefreshShowTranslationY);
      refreshing();
    } else if (state == STATE_DONE) {
      reset();
    }

    mState = state;
  }

  @Override public void onRefreshing() {
    setState(STATE_REFRESHING);
  }

  @Override public void onMove(float offSet) {
    int top = getTop();// 相对父容器listview的顶部位置 负数表示向上划出父容器的距离
    int currentHeight = getHeaderViewHeight();
    int targetHeight = currentHeight - (int) offSet;
    if (offSet < 0 && top == 0) { // 向下拉
      setHeaderViewHeight(targetHeight);
      refreshTranslation(currentHeight, offSet); // 向上推
    } else if (offSet > 0 && currentHeight > mHeaderViewHeight) {
      layout(getLeft(), 0, getRight(), targetHeight); //重新布局让header显示在顶端,直到不再缩小图片
      setHeaderViewHeight(targetHeight);
      refreshTranslation(currentHeight, offSet);
    }
  }

  /**
   * refreshView在刷新区间内相对位移并跟随位移速度旋转
   */
  private void refreshTranslation(int currentHeight, float offSet) {
    if ((currentHeight - mHeaderViewHeight) / 2 < mRefreshShowTranslationY - mRefreshHideTranslationY) { // 判断是否在非刷新区间
      float translationY = mRefreshView.getTranslationY() - offSet / 2; // 布局高度增加offset 相当于距离上边距offSet / 2
      if (translationY > mRefreshShowTranslationY) {
        translationY = mRefreshShowTranslationY;
      } else if (translationY < mRefreshHideTranslationY) {
        translationY = mRefreshHideTranslationY;
      }
      if (Math.abs(translationY) != mRefreshView.getTranslationY()) {
        mRefreshView.setTranslationY(translationY);
      }
    }
    mRefreshView.setRotation(mRotateAngle -= offSet);//旋转,角度大小跟随偏移量
  }

  @Override public boolean onRelease() {
    boolean isOnRefresh = false;
    int currentHeight = getLayoutParams().height;// 使用 mHeaderView.getLayoutParams().height 可以防止快速快速下拉的时候图片不回弹
    if (currentHeight > mHeaderViewHeight) {
      if ((currentHeight - mHeaderViewHeight) / 2 > mRefreshShowTranslationY - mRefreshHideTranslationY && mState < STATE_REFRESHING) {
        setState(STATE_REFRESHING);
        isOnRefresh = true;
      }
      headerRest();
    }
    if (!isOnRefresh && mRefreshView.getTranslationY() != mRefreshHideTranslationY) {
      refreshRest();
    }
    return isOnRefresh;
  }

  @Override public void refreshComplete() {
    mHandler.postDelayed(new Runnable() {
      public void run() {
        setState(STATE_DONE);
      }
    }, 200);
  }

  @Override public View getHeaderView() {
    return this;
  }

  @Override public int getOffset() {
    return getHeight() - mHeaderViewHeight;
  }

  private void refreshing() {
    mHandler.postDelayed(new Runnable() {
      @Override public void run() {
        if (mState == STATE_REFRESHING) {
          mRefreshView.setRotation(mRotateAngle += 8);
          mHandler.post(this);
        }
      }
    }, 50);
  }

  public void reset() {
    refreshRest();
    mHandler.postDelayed(new Runnable() {
      public void run() {
        setState(STATE_NORMAL);
      }
    }, 500);
  }

  private void headerRest() {
    ValueAnimator animator = ValueAnimator.ofInt(getLayoutParams().height, mHeaderViewHeight);
    //animator.setStartDelay(60);
    animator.setDuration(300).start();
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        if (getLayoutParams().height == mHeaderViewHeight) { // 停止动画,防止快速上划松手后动画产生抖动
          animation.cancel();
        } else {
          setHeaderViewHeight((Integer) animation.getAnimatedValue());
        }
      }
    });
  }

  private void refreshRest() {
    ValueAnimator animator = ValueAnimator.ofFloat(mRefreshView.getTranslationY(), mRefreshHideTranslationY);
    animator.setStartDelay(60);
    animator.setDuration(300).start();
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        if (mRefreshView.getTranslationY() == mRefreshHideTranslationY) {
          animation.cancel();
        } else {
          mRefreshView.setTranslationY((Float) animation.getAnimatedValue());
        }
      }
    });
  }
}

再放两张内容不足一屏的效果:

QQ空间
朋友圈
Demo地址

下一篇: RecyclerView实现QQ空间和微信朋友圈头部刷新效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值