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

标签: QQ空间刷新 微信朋友圈刷新 ListView
37人阅读 评论(0) 收藏 举报
分类:

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空间和微信朋友圈头部刷新效果

查看评论

大众点评的吃包子小人等待效果的实现

-
  • 1970年01月01日 08:00

仿微信朋友圈(QQ空间)下拉刷新(头部放大动画效果)

仿微信朋友圈下拉刷新(头部放大动画效果) 现在比较流行的Material Design控件已经可以实现更加炫酷的效果,以下我根据微信朋友圈(QQ空间也如此)原始的写法,基本可以达到预期效果。 先...
  • qwe490139301
  • qwe490139301
  • 2016-08-03 14:51:22
  • 2227

打造QQ空间头部视差ListView

QQ空间相信大家都用过,是否觉得它的下拉刷新很酷呢?今天就来自己实现这个控件。 首先看一下效果: 对实现过程不感兴趣的童鞋可以直接到文章底部粘帖代码,代码中有详细注释。 要实现这样的效果,需要重写...
  • u014165119
  • u014165119
  • 2015-07-31 14:36:02
  • 1581

Android UI设计之<十>自定义ListView,实现QQ空间阻尼下拉刷新和渐变菜单栏效果

好久没有写有关UI的博客了,刚刚翻了一下之前的博客,最近一篇有关UI的博客是在2014年写的:Android UI设计之自定义Dialog,实现各种风格效果的对话框。近来项目有个需求,要做个和QQ空间...
  • llew2011
  • llew2011
  • 2016-06-06 07:52:24
  • 9701

Android自定义ListView实现QQ空间顶部效果

Android自定义ListView实现QQ空间顶部效果转载请注明出处: (一) 效果图: QQ空间原图: (二)布局分析 1、首先先分析...
  • qq_23179075
  • qq_23179075
  • 2016-12-10 14:08:45
  • 1189

下拉放大图片

  • 2015年03月22日 20:35
  • 5.11MB
  • 下载

iOS开发010 tableView头部拉伸效果(类似QQ空间)

流行的一个拉伸效果 以及navibar的动态隐藏效果 下载地址: http://pan.baidu.com/s/1sj89gfV 测试环境:Xcode 6.2,iOS 7.0 以上...
  • moVing_BriCk
  • moVing_BriCk
  • 2015-10-16 17:47:31
  • 1175

下拉实现头部图片放大效果,实现类似QQ,新浪个人中心界面

今天要写的这个效果属于刷新类,比较实用,像很多流行的 app 都是用了这种效果,大家熟知的QQ空间、微博个人主页等。 本篇思路其实是完全按照android中已有的思路去实现的这种效果。 1.那么在...
  • eyeone
  • eyeone
  • 2016-09-18 00:09:12
  • 967

实现ios手机QQ空间导航栏控制器时隐时现效果,kvo的应用

//  这段代码可以实现导航栏时隐时现效果,kvo的应用 //  ViewController.m #import "ViewController.h" #import ...
  • py365367395
  • py365367395
  • 2015-12-18 02:12:58
  • 1230

实现 iOS 头部拉伸效果

主要涉及到导航栏透明度、图片拉伸、列表头部等。导航栏透明度的实现。 列表拖动距离的监听,及图片放大的实现。 导航透明度的设置 添加系统导航栏的Category实现 声明部分:@interface ...
  • u010596262
  • u010596262
  • 2017-11-02 09:14:02
  • 171
    个人资料
    专栏达人
    等级:
    访问量: 13万+
    积分: 1902
    排名: 2万+
    博客专栏
    最新评论