Android自定义SwipeRefreshLayout高仿微信朋友圈下拉刷新

上一篇文章里把SwipeRefreshLayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:http://www.jb51.net/article/89310.htm

上一篇里最后提到,SwipeRefreshLayout的可定制性是比较差的,看源码会发现跟样式相关的几个类都是private的而且方法是写死的,只暴露出了几个颜色设置的方法。这样使得SwipeRefreshLayout的使用比较简单,主要就是设置一个监听器在onRefresh方法里完成刷新逻辑。讲道理SwipeRefreshLayout的样式是挺美观的,如果以后都用这种下拉刷新样式的话,程序员就清静了,但这也是不太可能的。如果就想用官方的SwipeRefreshLayout,不想用第三方的控件,又想定制样式,该怎么办?基本上只能改源码了。下面就从修改源码的角度出发,给出自定义样式的思路。

首先需要将SwipeRefreshLayout以及内部使用到的CircleImageView和MaterialProgressDrawable的源码都拷贝出来,放到一个包里,方便修改。从源码可以知道,SwipeRefreshLayout中跟样式相关的类主要有两个:

一. CircleImageView,继承imageview,源码就不贴了,主要是绘制背景的,进度圈就是绘制在这上面,如果要修改进度圈的位置,就应该修改CircleImageView的位置。

二. MaterialProgressDrawable,继承Drawable实现Animatable接口,内部还定义了一个Ring类,主要是绘制进度圈的,如果要修改进度圈的图片和动画,就应该从这里开刀。

下面就以社交APP的BOSS微信为例,仿照朋友圈的下拉刷新效果。

先上效果图,可以跟手机里的微信比较一下,整体感觉还是可以的。第一次录gif,录了太长,处理的时候删了一些中间的帧)

这段时间在高仿微信,图方便就把整体的效果也展示了,读者关注刷新页面即可。布局主要就是一个SwipeRefreshLayout内嵌一个RecyclerView,滑动到顶端向下拖动时,出来的进度圈是朋友圈的那个彩虹圈,位置在左边,而且随着向下拖动会不断绕中心转啊转,此外,进度圈在到达某个位置后就不会再往下了。跟默认效果不同的还有recyclerview,默认是主布局是不会跟着拖动的,而微信的有一个拖动反弹效果,背景是黑色。开始刷新后,主布局反弹到头部,进度圈在那里转啊转,刷新完毕后进度圈就消失了,整个过程就是这样。那么就一步一步来.

1. 调整进度圈位置
 首先要将进度圈调整到左边,根据View的绘制原理,进度圈的位置应该是由父布局也就是SwipeRefreshLayout里的onLayout方法决定的,看看源码:

 @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  final int width = getMeasuredWidth();
  final int height = getMeasuredHeight();
  if (getChildCount() == 0) {
    return;
  }
  if (mTarget == null) {
    ensureTarget();
  }
  if (mTarget == null) {
    return;
  }
  final View child = mTarget;
  final int childLeft = getPaddingLeft();
  final int childTop = getPaddingTop();
  final int childWidth = width - getPaddingLeft() - getPaddingRight();
  final int childHeight = height - getPaddingTop() - getPaddingBottom();
  child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
  int circleWidth = mCircleView.getMeasuredWidth();
  int circleHeight = mCircleView.getMeasuredHeight();
  mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
      (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}

其中的mTarget就是主布局也就是recyclerview,而mCircleView就是转载进度圈的View,因此应该把最后一句注释掉,改为:

 //      mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
//          (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
//      修改进度圈的X坐标使之位于左边
      mCircleView.layout(childLeft, mCurrentTargetOffsetTop,
          childLeft+circleWidth, mCurrentTargetOffsetTop + circleHeight);

这样你就会很高兴地发现进度圈已经调到左边了。

2. 实现拖动反弹效果
 接下来先修改recyclerview的拖动反弹效果,SwipeRefreshLayout默认的效果是不拖动的,如果要修改其实也很简单,无非就是记录下手指运动的距离并让recyclerview设置translation就好了,那么找到onTouchEvent方法,修改ACTION_MOVE和ACTION_UP的部分:

  case MotionEvent.ACTION_MOVE: {
          pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
          if (pointerIndex < 0) {
            Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
            return false;
          }

          final float y = MotionEventCompat.getY(ev, pointerIndex);
//          记录手指移动的距离,mInitialMotionY是初始的位置,DRAG_RATE是拖拽因子,默认为0.5。
          final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
//          赋值给mTarget的top使之产生拖动效果
          mTarget.setTranslationY(overscrollTop);
          if (mIsBeingDragged) {
            if (overscrollTop > 0) {
              moveSpinner(overscrollTop);
            } else {
              return false;
            }
          }
          break;
        }
        case MotionEvent.ACTION_UP: {
//          手指松开时启动动画回到头部
          mTarget.animate().translationY(0).setDuration(200).start();

          pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
          if (pointerIndex < 0) {
            Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
            return false;
          }

          final float y = MotionEventCompat.getY(ev, pointerIndex);
          final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
          mIsBeingDragged = false;
          finishSpinner(overscrollTop);
          mActivePointerId = INVALID_POINTER;
          return false;
        }

不相关的我都略过了,修改的地方我也注释了,很清晰。这样就解决了拖动反弹的问题,得益于SwipeRefreshLayout的框架,不用考虑冲突问题,修改起来还是很简单的。

3. 修改图标和拖动时的动画 

接下来就是比较麻烦的图标和动画了。修改图标其实不难,因为CircleView是继承ImageView的,完全可以通过反射取到CircleView的实例变量,然后setBitmap将你的图标传进去。但是这样的话就没有动画了,显然也是没啥意义的。读者可以大致看看MaterialProgressDrawable的源码,要实现默认的动画还是比较复杂的,我这里要改为微信的效果,就一个圈圈转啊转,还是比较简单的,下面就结合上篇文章所解析的流程看看如何修改。
 首先新建一个CustomProgressDrawable类,并继承自MaterialProgressDrawable(需要将源码复制出来),还需要在SwipeRefreshLayout添加set方法,方便把自定义的类传进去。

 public void setProgressView(MaterialProgressDrawable mProgress){
  this.mProgress = mProgress;
  mCircleView.setImageDrawable(mProgress);
}

要在CustomProgressDrawable中绘制自定义的图标,就需要暴露一个setBitmap的方法以便绘制。上篇文章提到,手指移动时会调用moveSpinner方法,并把移动的距离传进去,该方法内首先会经过一堆数学的处理得出一个rotation,再把它传入mProgress的setProgressRotation,也就是说setProgressRotation方法是通过传入的角度来转圈圈的。朋友圈的效果就是一直让中心转,所以很容易改写:

 private float rotation;
  private Bitmap mBitmap;

  public void setBitmap(Bitmap mBitmap) {
    this.mBitmap = mBitmap;
  }

  @Override
  public void setProgressRotation(float rotation) {
//    取负号是为了和微信保持一致,下拉时逆时针转加载时顺时针转,旋转因子是为了调整转的速度。
    this.rotation = -rotation*ROTATION_FACTOR;
    invalidateSelf();
  }

  @Override
  public void draw(Canvas c) {
    Rect bound = getBounds();
    c.rotate(rotation,bound.exactCenterX(),bound.exactCenterY());
    Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
    c.drawBitmap(mBitmap,src,bound,paint);
  }

就是不断旋转canvas再绘制bitmap。这样你就会很高兴地发现下拉的时候圈圈也转起来了。

4. 设置进度圈下拉界限和实现加载时的动画
此时正在刷新的时候圈圈是不会转的,而且圈圈默认是跟着手指拖动的,没有界限,而朋友圈的效果是圈圈在下拉到一个位置后就不再继续下拉了,先来解决下拉位置的问题。
 在moveSpinner方法中,调用完setProgressRotation方法来转圈后,就会调用setTargetOffsetTopAndBottom来改变mProgress的位置,代码就不贴了。既然我们要限定下拉的位置,那就应该在这里加以限制,当下移到刷新的位置时就不再下移了,代码如下:

 private void moveSpinner(float overscrollTop) {
…
//      setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
//      最终刷新的位置
      int endTarget;
      if (!mUsingCustomStart) {
//        没有修改使用默认的值
        endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop));
      } else {
//        否则使用定义的值
        endTarget = (int) mSpinnerFinalOffset;
      }
      if(targetY>=endTarget){
//        下移的位置超过最终位置后就不再下移,第一个参数为偏移量
        setTargetOffsetTopAndBottom(0, true /* requires update */);
      }else{
//        否则继续继续下移
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
      }
}

这里先计算出一个endTarget,就是最终的位置,其他注释的比较详细不说了,这样就限制住了下移的位置。
 接下来要让刷新的时候圈圈继续转,那就需要知道刷新时是执行哪里的动画。上篇文章也提到了,转圈的动画是在mProgress的start方法里的,来看看源码:

 @Override
public void start() {
  mAnimation.reset();
  mRing.storeOriginals();
  // Already showing some part of the ring
  if (mRing.getEndTrim() != mRing.getStartTrim()) {
    mFinishing = true;
    mAnimation.setDuration(ANIMATION_DURATION/2);
// 将转圈圈的动画传入
    mParent.startAnimation(mAnimation);
  } else {
    mRing.setColorIndex(0);
    mRing.resetOriginals();
    mAnimation.setDuration(ANIMATION_DURATION);
// 将转圈圈的动画传入
    mParent.startAnimation(mAnimation);
  }
}

主要其实就最后一句,将转圈圈的动画传入,mAnimation就是默认的转动动画,感兴趣可以自己去看看,我们只需要自定义转圈圈的动画并传入该方法就可以了。有了刚才的setProgressRotation方法,只需要定义一个动画并不断改变rotation的值并执行这个方法就好了,代码如下:

 private void setupAnimation() {
//    初始化旋转动画
    mAnimation = new Animation(){
      @Override
      protected void applyTransformation(float interpolatedTime, Transformation t) {
        setProgressRotation(-interpolatedTime);
      }
    };
    mAnimation.setDuration(5000);
//    无限重复
    mAnimation.setRepeatCount(Animation.INFINITE);
    mAnimation.setRepeatMode(Animation.RESTART);
//    均匀转速
    mAnimation.setInterpolator(new LinearInterpolator());
  }

  @Override
  public void start() {
    mParent.startAnimation(mAnimation);
  }

这样就OK了!

5. 修改加载完毕的动画
 现在已经基本完成了,最后还有一个结束的动画,默认是scale动画,而微信的是向上运动至消失,最后的动画是通过执行SwipeRefreshLayout的startScaleDownAnimation方法完成的,在方法内部定义了一个scale动画,我们只需要注释掉并自己定义一个动画就好了:

 private void startScaleDownAnimation(Animation.AnimationListener listener) {
//      mScaleDownAnimation = new Animation() {
//        @Override
//        public void applyTransformation(float interpolatedTime, Transformation t) {
//          setAnimationProgress(1 - interpolatedTime);
//        }
//      };

//      最终的偏移量就是mCircleView距离顶部的高度
      final int deltaY = -mCircleView.getBottom();
      mScaleDownAnimation = new TranslateAnimation(0,0,0,deltaY);
//      mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION);
      mScaleDownAnimation.setDuration(500);
      mCircleView.setAnimationListener(listener);
      mCircleView.clearAnimation();
      mCircleView.startAnimation(mScaleDownAnimation);
    }

也就是一个偏移动画~
 在activity中进行一些设置,传入朋友圈的图标后就能得到开头的效果了:

 CustomProgressDrawable drawable = new CustomProgressDrawable(this,mRefreshLayout);
  Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.moments_refresh_icon);
  drawable.setBitmap(bitmap);
  mRefreshLayout.setProgressView(drawable);
  mRefreshLayout.setBackgroundColor(Color.BLACK);
  mRefreshLayout.setProgressBackgroundColorSchemeColor(Color.BLACK);
  mRefreshLayout.setOnRefreshListener(new CustomSwipeRefreshLayout.OnRefreshListener(){
    @Override
    public void onRefresh() {
      final Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
          super.handleMessage(msg);
            mRefreshLayout.setRefreshing(false);
        }
      };
      new Thread(new Runnable() {
        @Override
        public void run() {
          try {
//  在子线程睡眠三秒后发送消息停止刷新。
            Thread.sleep(3000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          handler.sendEmptyMessage(0);
        }
      }).start();
    }
  });

以上就基本通过修改SwipeRefreshLayout的源码仿照了朋友圈的下拉刷新效果了。从源码可以看出SwipeRefreshLayout确实是写得比较封闭的,不修改源码是基本没法自定义样式的,不过这样跟着源码过了一遍思路就比较清晰了。以后如果有机会再试着封装一下吧~

最后再附上CustomProgressDrawable的完整代码吧。SwipeRefreshLayout的太长就不发了,该改的地方应该都提到了。

public class CustomProgressDrawable extends MaterialProgressDrawable{

//  旋转因子,调整旋转速度
  private static final int ROTATION_FACTOR = 5*360;
//  加载时的动画
  private Animation mAnimation;
  private View mParent;
  private Bitmap mBitmap;
//  旋转角度
  private float rotation;
  private Paint paint;

  public CustomProgressDrawable(Context context, View parent) {
    super(context, parent);
    mParent = parent;
    paint = new Paint();
    setupAnimation();
  }

  private void setupAnimation() {
//    初始化旋转动画
    mAnimation = new Animation(){
      @Override
      protected void applyTransformation(float interpolatedTime, Transformation t) {
        setProgressRotation(-interpolatedTime);
      }
    };
    mAnimation.setDuration(5000);
//    无限重复
    mAnimation.setRepeatCount(Animation.INFINITE);
    mAnimation.setRepeatMode(Animation.RESTART);
//    均匀转速
    mAnimation.setInterpolator(new LinearInterpolator());
  }

  @Override
  public void start() {
    mParent.startAnimation(mAnimation);
  }
  public void setBitmap(Bitmap mBitmap) {
    this.mBitmap = mBitmap;
  }

  @Override
  public void setProgressRotation(float rotation) {
//    取负号是为了和微信保持一致,下拉时逆时针转加载时顺时针转,旋转因子是为了调整转的速度。
    this.rotation = -rotation*ROTATION_FACTOR;
    invalidateSelf();
  }

  @Override
  public void draw(Canvas c) {
    Rect bound = getBounds();
    c.rotate(rotation,bound.exactCenterX(),bound.exactCenterY());
    Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
    c.drawBitmap(mBitmap,src,bound,paint);
  }
}

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

(0)

相关推荐

  • Android仿微信滑动弹出编辑、删除菜单效果、增加下拉刷新功能

    如何为不同的list item呈现不同的菜单,本文实例就为大家介绍了Android仿微信或QQ滑动弹出编辑.删除菜单效果.增加下拉刷新等功能的实现,分享给大家供大家参考,具体内容如下 效果图: 1. 下载开源项目,并将其中的liberary导入到自己的项目中: 2. 使用SwipeMenuListView代替ListView,在页面中布局: <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swipeRefresh

  • Android微信端的下拉刷新功能

    在Android和iOS上对于下拉刷新的处理方法: 在微信公众号内,在面对下拉刷新这个问题上,Android和iOS都自己的表现方式: iOS: Android: 所以我们要给内容加载监听器 function bindEvent() { document.addEventListener('touchstart', touchSatrtFunc, false); document.addEventListener('touchmove', touchMoveFunc, false); docum

  • Android自定义SwipeRefreshLayout高仿微信朋友圈下拉刷新

    上一篇文章里把SwipeRefreshLayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:http://www.jb51.net/article/89310.htm 上一篇里最后提到,SwipeRefreshLayout的可定制性是比较差的,看源码会发现跟样式相关的几个类都是private的而且方法是写死的,只暴露出了几个颜色设置的方法.这样使得SwipeRefreshLayout的使用比较简单,主要就是设置一个监听器在onRefresh方法里完成刷新逻辑.讲道理Swip

  • Android 高仿微信朋友圈拍照上传功能

    模仿微信朋友圈发布动态,输入文字支持文字多少高度自增,有一个最小输入框高度,输入文字有限制,不过这些都很easy! 1. PhotoPicker的使用 这是一个支持选择多张图片,点击图片放大,图片之间左右滑动互相切换的库,同时支持图片删除的库,效果类似微信. (1) 添加PhotoPicker的架包 (2) 使用 选择图片:安卓6.0以后需要在代码中添加读写sd卡和相机的权限 当然清单文件中也需要添加的 PhotoPicker.builder() .setPhotoCount(maxPhoto)

  • Android 高仿微信朋友圈动态支持双击手势放大并滑动查看图片效果

    最近参与了开发一款旅行APP,其中包含实时聊天和动态评论功能,终于耗时几个月几个伙伴完成了,今天就小结一下至于实时聊天功能如果用户不多的情况可以scoket实现,如果用户万级就可以采用开源的smack + opnefile实现,也可以用mina开源+XMMP,至于怎么搭建和实现,估计目前github上一搜一大把,至于即时通讯怕误人子弟,暂且不做介绍,现就把实现的一个微信朋友圈的小功能介绍一下. 先上效果图: 一拿到主流的UI需求,大致分析下,需要我ListView嵌套Gridview,而grid

  • Android+Html5混合开发仿微信朋友圈

    开发之前 大约从去年开始吧, 也可能是前年 Html5好像火得不得了, 不得了...总能从网上听说到 XXX混合开发, 为了紧跟潮流(虽然有点儿晚了), 咱们也看看Android+Html5混合开发是怎样的! 今天带来的案例是微信的朋友圈, 因为我觉得是微信把H5给"捧红了". 不过丑话说在前头, 咱们的仿朋友圈可是"低仿", 只是把混合开发的大致流程说说, 界面可能不堪入目...见谅.. 开发环境 Android Studio 2.2.2 JDK1.7 API 2

  • Android自定义TextView仿微信朋友圈文字展开全文功能

    Android自定义TextView仿微信朋友圈文字信息,展开全文功能 代码及注释如下: 首先写一个xml文件 showmore.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical

  • Android 仿微信朋友圈点赞和评论弹出框功能

    贡献/下载源码:https://github.com/mmlovesyy/PopupWindowDemo 本文简单模仿微信朋友圈的点赞和评论弹出框,布局等细节请忽略,着重实现弹出框.发评论,及弹出位置的控制. 1. 微信弹出框 微信朋友圈的点赞和评论功能,有2个组成部分: 点击左下角的"更多"按钮,弹出对话框: 点击评论,弹出输入框,添加评论并在页面中实时显示: 微信朋友圈点赞和评论功能 2. 实际效果 本文将建一个 ListView,在其 Item 中简单模仿微信的布局,然后着重实现

  • Android仿微信朋友圈全文、收起功能的实例代码

    前言 一般在社交APP中都有类似朋友圈的功能,其中发表的动态内容很长的时候不可能让它全部显示.这里就需要做一个仿微信朋友圈全文.收起功能来解决该问题.在网上看到一个例子-->http://www.jb51.net/article/105251.htm,写的很不错,但是有个bug,他这个Demo只有在条目固定的时候才正常,当增加.删除条目的时候会出现全文.收起显示混乱的问题.原因是他使用了固定的position作为key来保存当前显示的状态.这篇文章在他的基础上进行优化. 效果图 具体代码 (详细

  • uniapp 仿微信的右边下拉选择弹出框的实现代码

    在百度找了很多 没有找到满意的 这里根据自己的需求 抽取一个组件 这个组件主要是包括搜索框和右边菜单点击弹出一个下拉筛选菜单 这里首先用一个单独的页面存放这个组件 <template> //这里是搜索框的输入框 不需要的可以删掉 <view> <view class="arrivalSearch"> <view class="arrivalSmallsearch"> <view class="arriv

  • 微信小程序 下拉刷新及上拉加载原理解析

    这篇文章主要介绍了微信小程序 下拉刷新及上拉加载实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.下拉刷新的概念及应用场景. 概念: 下拉刷新是移动端更新列表数据的交互行为,用户通过手指在屏幕上子上而下的滑动,可以触发页面的下拉刷新,更新列表数据. 应用场景: 在移动端,数据列表是常见的页面效果,更新列表数据是最基本的页面需求,相比于按钮刷新,定时刷新来说,下拉刷新的用户体验方便友好,已经成为移动端刷新列表数据的最佳解决方案. 微信小

随机推荐