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

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

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

老规矩先上图
QQ空间
朋友圈
本篇主要讲RecyclerView实现QQ空间和微信朋友圈头部刷新效果,如果想了解ListView如何实现,请查看上篇:ListView实现QQ空间和微信朋友圈头部刷新效果

这是Demo地址

按照套路,实现上述效果需要重新自定义一个RecyclerView,但是依照不重复定义轮子的原则(前提是了解实现原理),我选择在LRecyclerView的基础上二次改造实现上面效果。

PS: LRecyclerView的主要原理是内部封装了一个warpAdapter,将真正的用户写的adapter包装成innerAdapter,warpAdapter可以添加Header、Footer等类型的Item,根据添加的数量做position的偏移,再将偏移后的数据传给真正的adapter(innerAdapter),就如使用原生Adapter一样,可以理解为做了一个中间的适配器。

LRecyclerView的改造

主要修改的内容

一. 重写了滑动过度方法overScrollBy(RecyclerView的overScrollBy方法不像ListView在父类AbsListView中有重写,实际上不起作用,可以不必重写,随便添加个方法就行,这里为了和上篇对应)。

  @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) {
      mRefreshHeader.onMove(deltaY, sumOffSet);
    }
    return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
  }

二. 重写了onTouchEvent方法,增加了多点触摸的判断。判读并调用上面的滑动过度方法overScrollBy

修改前后对比图:
这里写图片描述
代码:


  private int mActivePointerId;

  @Override public boolean onTouchEvent(MotionEvent ev) {
    if (mLastY == -1) { // 如果adapter设置了setOnItemClickListener点击事件,则RecyclerView的ACTION_DOWN事件被拦截,这里通过这种方式获取起始坐标。
      mLastY = ev.getY();
      mActivePointerId = ev.getPointerId(0);
      sumOffSet = 0;
    }
    switch (ev.getActionMasked()) {
      case MotionEvent.ACTION_DOWN:
        mLastY = ev.getY();
        mActivePointerId = ev.getPointerId(0);
        sumOffSet = 0;
        break;
      case MotionEvent.ACTION_POINTER_DOWN:
        final int index = ev.getActionIndex();
        mActivePointerId = ev.getPointerId(index);
        mLastY = (int) ev.getY(index);
        break;
      case MotionEvent.ACTION_MOVE:
        int pointerIndex = ev.findPointerIndex(mActivePointerId);
        if (pointerIndex == -1) {
          pointerIndex = 0;
          mActivePointerId = ev.getPointerId(pointerIndex);
        }
        final int moveY = (int) ev.getY(pointerIndex);
        final float deltaY = (mLastY - moveY) / DRAG_RATE;
        mLastY = moveY;
        sumOffSet += deltaY;
        if (isOnTop() && mPullRefreshEnabled && !mRefreshing && (appbarState == AppBarStateChangeListener.State.EXPANDED)) {
          if (deltaY < 0 && !canScrollVertically(-1) || deltaY > 0 && !canScrollVertically(1)) { //判断无法下拉和无法上拉(item过少的情况)
            overScrollBy(0, (int) deltaY, 0, 0, 0, 0, 0, (int) sumOffSet, true);
          }
        }
        break;
      case MotionEvent.ACTION_UP:
        mLastY = -1; // reset
        mActivePointerId = -1;
        if (isOnTop() && mPullRefreshEnabled && !mRefreshing/*&& appbarState == AppBarStateChangeListener.State.EXPANDED*/) {
          if (mRefreshHeader != null && mRefreshHeader.onRelease()) {
            if (mRefreshListener != null) {
              mRefreshing = true;
              mFootView.setVisibility(GONE);
              mRefreshListener.onRefresh();
            }
          }
        }
        break;
    }
    return super.onTouchEvent(ev);
  }

三. 修改onScrolled方法,添加上推缩小的方法:

  @Override public void onScrolled(int dx, int dy) {
    super.onScrolled(dx, dy);
    ...
    if (isOnTop() && mPullRefreshEnabled && !mRefreshing && (appbarState == AppBarStateChangeListener.State.EXPANDED)) {
      if (dy > 0) {
        mRefreshHeader.onMove(dy, mScrolledYDistance);
      }
    }
  }

PS:这里在onScrolled中添加上推缩小的方法而不是直接在onTouchEvent中使用是因为super.onTouchEvent(ev)会调用startInterceptRequestLayout()阻断子View的布局请求,然后通过RecyclerView当前LayoutManagerRecyclerView执行滑动操作,接着执行stopInterceptRequestLayout()停止阻断子View的布局请求,最终会执行onScrolled方法。如果直接在onTouchEvent中上推缩小则会因为前面的原因产生Header抖动。如果判断条件在ACTION_MOVE返回true不调用父类方法super.onTouchEvent(ev)则又会因为super.onTouchEvent(ev)中ACTION_MOVE拿不到实时位置,最终会产生抖动~。

重点在这里

主要原理:
1. 给LRecyclerview添加刷新视图(setRefreshHeader)。
2. 头部视图主要由一个头图(android:scaleType="centerCrop"用来缩放)和提示刷新状态的图片构成。
3. 重写滑动过度回调方法overScrollBy,并在onTouchEvent方法中判断是否下拉过度(canScrollVertically(-1)),然后调用overScrollBy让头图的布局高度增加并请求重新布局requestLayout(),实现头图拉伸。
4. 头部拉伸后上推,如果此时LRecyclerview的高度超过屏幕则通过onScrolled判断滑动距离(dy > 0 向上滑动 dy < 0 向下滑动,这里需要 dy > 0)。
5. 头部拉伸后上推,如果此时LRecyclerview的高度不够一屏,则LRecyclerview无法滑动,不能通过onScrolled判断,需要在onTouchEvent方法中判断是否上拉过度(canScrollVertically(1)),然后请求重新布局,减少头图高度,实现头图高度缩小。
6. 随着头部的拉伸和收缩实现刷新状态图片的位置变化和旋转,旋转角度和方向跟随头图每次拉伸和收缩的增量(负数下拉,正数上推),限制图片跟随头图下拉的高度。

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

下面使用的IRefreshHeader 来自LRecyclerView。

QQ空间头部刷新效果实现

Recyclerview的QQ空间头部刷新效果实现和ListView是一样的步骤。

首先是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>

注意头部布局中头图的缩放方式。这里的布局和上篇ListView使用的同一个布局。

然后是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();
  }

  public QzoneRefreshHeader(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
  }

  private void initView() {
    LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    lp.setMargins(0, 0, 0, 0);
    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();
  }

  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 refreshComplete() {
    mHandler.postDelayed(new Runnable() {
      public void run() {
        setState(STATE_DONE);
      }
    }, 200);
  }

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

  @Override public int getVisibleHeight() {
    return getHeight();
  }

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

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

  //垂直滑动时该方法不实现
  @Override public int getVisibleWidth() {
    return 0;
  }

  @Override public void onReset() {
    setState(STATE_NORMAL);
  }

  @Override public void onPrepare() {
    setState(STATE_RELEASE_TO_REFRESH);
  }

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

  @Override public void onMove(float offSet, float sumOffSet) {
    int top = getTop();// 相对父容器recyclerview的顶部位置 负数表示向上划出父容器的距离
    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;
  }

  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.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 refreshing() {
    mHandler.postDelayed(new Runnable() {
      @Override public void run() {
        if (mState == STATE_REFRESHING) {
          mRefreshView.setRotation(mRotateAngle += 8);
          mHandler.post(this);
        }
      }
    }, 50);
  }

  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();
  }

  public MomentsRefreshHeader(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
  }

  private void initView() {
    LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    lp.setMargins(0, 0, 0, 0);
    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();
  }

  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 refreshComplete() {
    mHandler.postDelayed(new Runnable() {
      public void run() {
        setState(STATE_DONE);
      }
    }, 200);
  }

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

  @Override public int getVisibleHeight() {
    return getHeight();
  }

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

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

  //垂直滑动时该方法不实现
  @Override public int getVisibleWidth() {
    return 0;
  }

  @Override public void onReset() {
    setState(STATE_NORMAL);
  }

  @Override public void onPrepare() {
    setState(STATE_RELEASE_TO_REFRESH);
  }

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

  @Override public void onMove(float offSet, float sumOffSet) {
    int top = getTop();// 相对父容器recyclerview的顶部位置 负数表示向上划出父容器的距离
    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;
  }

  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.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 refreshing() {
    mHandler.postDelayed(new Runnable() {
      @Override public void run() {
        if (mState == STATE_REFRESHING) {
          mRefreshView.setRotation(mRotateAngle += 8);
          mHandler.post(this);
        }
      }
    }, 50);
  }

  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地址

查看评论

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

实现大众点评吃包子小人等待效果
  • 2015年03月23日 10:32

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

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

打造QQ空间头部视差ListView

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

ScrollView的阻尼回弹效果实现(仿qq空间)

玩过新浪微博,qq空间等手机客户端的童鞋,都应该清楚,在主界面向下滑动时,会有一个阻尼回弹效果,看起来挺不错,接下来我们就来实现一下这种效果,下拉后回弹刷新界面,先看效果图: 这个是编辑器里面的界面效...
  • baiyuliang2013
  • baiyuliang2013
  • 2014-05-14 17:17:40
  • 6064

优雅地为RecyclerView加上头部、下拉刷新、自动加载

优雅地为RecyclerView加上头部、下拉刷新、自动加载更多
  • u011310942
  • u011310942
  • 2016-11-12 15:46:38
  • 3222

一个RecyclerView实现QQ空间相册布局

Android RecyclerView QQ空间相册
  • forvv231
  • forvv231
  • 2017-07-08 11:01:07
  • 451

支持下拉刷新和上划加载更多的自定义RecyclerView(仿XListView效果)

在项目更新的过程中,遇到了一个将XListView换成recyclerView的需求,而且更换完之后大体效果不能变,但是对于下拉刷新这样的效果,谷歌给出的解决方案是把RecyclerView放在一个S...
  • lvshaorong
  • lvshaorong
  • 2016-05-18 10:44:42
  • 9998

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
  • 1176

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

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

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

//  这段代码可以实现导航栏时隐时现效果,kvo的应用 //  ViewController.m #import "ViewController.h" #import ...
  • py365367395
  • py365367395
  • 2015-12-18 02:12:58
  • 1232
    个人资料
    专栏达人
    等级:
    访问量: 13万+
    积分: 1902
    排名: 2万+
    博客专栏
    最新评论