Android自定义SwipeRefreshLayout * 微信朋友圈下拉刷新
作者:u011443509 发布时间:2023-01-06 08:51:34
上一篇文章里把SwipeRefreshLayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:https://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);
}
}
来源:http://blog.csdn.net/u011443509/article/details/52025019
猜你喜欢
- 本文实例讲述了java实现的n*n矩阵求值及求逆矩阵算法。分享给大家供大家参考,具体如下:先来看看运行结果:java版的写出来了,用的跟c语
- java中Hashmap的get方法map中存储的是键值对,也就是说通过set方法进行参数和值的存储,之后通过get“键”的形式进行值的读取
- spring cloud zuul增加header传输在使用OAuth2.0传输权限认证,为了再调用其他的项目的时候获取token,必须在t
- 译文链接: https://www.infoworld.com/art...AutoMapper 是一个非常流行的 object-to-ob
- java 算法之归并排序详解一、思想 归并排序:将一个数组排序,可以先(递归地)将它分成两半部份分别排序,然后将结果归并起来; &
- 方式1:1. 明确 Spark中Job 与 Streaming中 Job 的区别1.1 Spark Core一个 RDD DAG Graph
- 一、基本介绍(Nexus(maven * ))1,如果没有搭建 * 会有什么问题?如果没有 * ,我们所需的所有构件都需要通过 Mave
- ubuntu 安装jdk 的两种方法总结:1:通过ppa(源) 方式安装.2:通过官网下载安装包安装.这里推荐第1种,因为可以通过 apt-
- 这里设计一个自定义View,继承了ScrollView,实现可以下拉里面的内容,松手后画面弹回,这个自定义的View可以当做ScrollVi
- 本文实例讲述了Java Swing组件编程之JTable表格用法。分享给大家供大家参考,具体如下:表格是GUI编程中使用较多,但也是最麻烦的
- 包括了写入和读取功能. 写入的时候, 如果文件不存在会自动创建. 如果对应的键已经存在, 则自动覆盖它的值. 读取的时候, 如果对应的文件不
- 本文将介绍使用Spring Boot集成Mybatis并实现主从库分离的实现(同样适用于多数据源)。延续之前的Spring Boot 集成M
- 大家好,我是梦辛工作室的灵,最近在帮客户修改安卓程序时,有要求到一个按钮要浮动在键盘的上方,下面大概讲一下实现方法:其实很简单,分三步走第一
- 在安卓开发中,会碰到选开始日期和结束日期的问题。特别是在使用Pad时,如果弹出一个Dialog,能够同时选择开始日期和结束日期,那将是极好的
- 一、什么是热部署?热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。二、什么是SpringBoot热部署?SpringBoot
- 下面是一个AOP实现的简单例子:首先定义一些业务方法:/** * Created with IntelliJ IDEA. 
- 前言之前几篇我们介绍了贝塞尔曲线的原理、绘制曲线和动效实现,这些都是代码预设好的,如果我们要根据需要自行绘制曲线,就需要使用交互来实现了。本
- 这个导出网站功能指通过前台javascript触发进入ashx函数中,实现将服务器中某个文件夹(包含其子文件夹和文件)通通复制到服务器中另一
- 在使用多线程的时候有时候我们会使用 java.util.concurrent.Executors的线程池,当多个线程异步执行的时候,我们往往
- 本文实例讲述了C#使用iTextSharp设置PDF所有页面背景图功能的方法。分享给大家供大家参考。具体如下:在生成PDF 的时候,虽然可以