高性能的给RecyclerView添加下拉刷新和加载更多动画,基于drawable(二)
2017-10-04 14:43
423 查看
项目已经上传github,点击这里查看
先看看效果
![](https://img-blog.csdn.net/20171004145915812?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdGluZ2Zlbmd6aGVzaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
![](https://img-blog.csdn.net/20171004145924067?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdGluZ2Zlbmd6aGVzaHVv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
动画很粗糙,请不要在意。
项目是基于我改进的一个RecyclerView.Adapter,这个adapter可以给RecyclerView添加header和footer,关于这个adapter,可以点击查看
实现的逻辑是,给RecyclerView各添加一个自定义View作为Header和Footer,自定义的view作为Drawable的Drawable.callback对象,这样drawable不断改变自身
实现动画效果。
动画实现了,那么刷新时列表时列表怎么向下移动呢?这需要说说RecyclerView和LayoutManager的关系。
如果你不熟悉RecyclerView和LayoutManager的关系,你可以阅读国外大神dave smith的博文
http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
http://wiresareobsolete.com/2014/09/recyclerview-layoutmanager-2/
http://wiresareobsolete.com/2015/02/recyclerview-layoutmanager-3/
如果你不读,那没关系,我告诉你,RecyclerView的layout是被LayoutManager代理了,另外你在滑动列表的时候,RecyclerView的layout过程是不会走的,这可以提高性能。但是如果我们想让RecyclerView重新计算child的大小,那么需要调用notifyDataChanged方法了。
现在我们的思路是,随着手指滑动,我们改变HeaderView的高度,调用notifyDataSetChanged,这样就能使列表向下滑动。
先看看自定义的View
我们重写了onMeasure方法,让view的高度是mHeight的值。setDrawable方法中把view自身设为Drawable对象的Callback对象。
这是我们需要用到的Drawable对象,其中的percent和我们说的前景实现一样,是描述刷新(加载)完成度的一个度量值。setPercent中调用adapter的notifyDataSetChanged。里面的dWidth和dHeight必须被设置,否则drawable无法绘制。
这里面要说一下为什么把最小的高度值设为0而不是1,是因为我发现一个bug,如果高度设为0 ,那么下面这个方法会工作不正常。这个bug导致和LinearLayouManagert的逻辑和每16ms绘制一次界面的机制有关系。有兴趣的可以查看下android源码,我并没有进一步测试。
这个是Adapter实现了setDrawable的时候先添加一个View为Header,这个View也是drawable的载体。
这是自定义的RecyclerView,重写了setAdatper,如果设置的adapter是支持刷新和加载更多的adapter那么就设置默认的drawable。
啊,刚吃完中秋晚饭,继续写。
下面我们要说说自定义的RecyclerView,这个自定义的RecyclerView和前景实现中自定义的RecyclerView的手势处理差不多,就是有一些细节需要处理。下面是整个
自定义的RecyclerView的代码
AdvancedDrawableRecyclerView这个类的onTouchEvent中有一个细节,就是处理加载更多时的手势代码中,有这两句
LayoutManager代理RecyclerView的layout过程,而这里有一个被称为锚的类,这个类可以标记RecyclerView正在展示的child从哪开始,而child在上拉的过程中,如果不偏移,重绘的时候这个起始点的信息不会变,那么上面列表的高度没有变化,那么列表就不会随着手指向上移动,虽然footer高度变化了,但是看不出来,所以需要偏移去修正。
手势处理过程如下,和前景实现保持一致。
手势监听
|
| 如果如果手势向下,view不能向下继续滑动
|
计算滑动的距离,当滑动距离没有到阀值的时候,根据滑动距离和
最大滑动距离的百分比,计算绘制的图案位置,圆弧的角度等等
| |
| 当滑动时未超过阀值松开 | 超过阀值时松开
| |
这个时候让图案回弹就Ok 图案先回弹到阀值对应的位置,然后开始旋转,
当刷新完成的时候,回弹
手势过程中平移的百分比计算代码中不难理解,另外在前景实现中已经说过了,所以这儿不说了,可以参考前一篇文章。
我们说说手指抬起来后发生了什么
很简单,如果超过开始刷新(加载更多)的临界值,就开启回到临界位置的动画,否则就开启回到初始位置的动画
这是回弹到临界位置的动画逻辑,这里面,需要更新时的计算出来的值就是Percent,然后把percent传递给Drawable,并且修改Drawable对应的view的高度。
回到初始位置的动画逻辑类似
我们继续看看drawable是如何根据percent值绘制的。
这是我自定义的一个drawable,对应的是默认的刷新的动画。
这是根据github上优秀项目pullToRefresh改写的,在这里感谢pullToRefresh项目作者。
我们继续说源码,自定义的drawable中根据传进来的View初始化了相关数据。
draw的过程分为两步,绘制天空,绘制太阳。
关于刷新时太阳旋转的动画实现
开启一个ValueAnimator,这个ValueAnimator用来计算每个时间点对应的角度值。这样随着时间的流逝,太阳就转动了。
如果你想自定义Drawable,你只需要创建一个继承AdvancedDrawable的类,实现相关方法,然后把类实例设置给Adapter就好。
好了,这个项目差不多就是这样了,欢迎各位留言交流。
先看看效果
动画很粗糙,请不要在意。
项目是基于我改进的一个RecyclerView.Adapter,这个adapter可以给RecyclerView添加header和footer,关于这个adapter,可以点击查看
实现的逻辑是,给RecyclerView各添加一个自定义View作为Header和Footer,自定义的view作为Drawable的Drawable.callback对象,这样drawable不断改变自身
实现动画效果。
动画实现了,那么刷新时列表时列表怎么向下移动呢?这需要说说RecyclerView和LayoutManager的关系。
如果你不熟悉RecyclerView和LayoutManager的关系,你可以阅读国外大神dave smith的博文
http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/
http://wiresareobsolete.com/2014/09/recyclerview-layoutmanager-2/
http://wiresareobsolete.com/2015/02/recyclerview-layoutmanager-3/
如果你不读,那没关系,我告诉你,RecyclerView的layout是被LayoutManager代理了,另外你在滑动列表的时候,RecyclerView的layout过程是不会走的,这可以提高性能。但是如果我们想让RecyclerView重新计算child的大小,那么需要调用notifyDataChanged方法了。
现在我们的思路是,随着手指滑动,我们改变HeaderView的高度,调用notifyDataSetChanged,这样就能使列表向下滑动。
先看看自定义的View
public class DrawableView extends View { private Drawable mDrawable; private int mHeight = 1; public DrawableView(Context context) { super(context); } public void setHeight(int height){ if (mHeight == height)return; if (height == 0){ height = 1; } mHeight = height; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mHeight,MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, newHeightMeasureSpec); } public void setDrawable(Drawable drawable){ mDrawable = drawable; mDrawable.setCallback(this); } int getCurrentHeight(){ return mHeight; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mDrawable == null) { return; // couldn't resolve the URI } if (mDrawable.getIntrinsicWidth() <= 0 || mDrawable.getIntrinsicHeight() <= 0) { return; // nothing to draw (empty bounds) } canvas.clipRect(getPaddingLeft(), getPaddingTop(), getRight() - getLeft() - getPaddingRight(), getBottom() - getTop() - getPaddingBottom()); int saveCount = canvas.save(); mDrawable.draw(canvas); canvas.restoreToCount(saveCount); } @Override public void invalidateDrawable(Drawable dr) { if (dr == mDrawable) { invalidate(); } else { super.invalidateDrawable(dr); } } }
我们重写了onMeasure方法,让view的高度是mHeight的值。setDrawable方法中把view自身设为Drawable对象的Callback对象。
public abstract class AdvancedDrawable extends Drawable implements Animatable { int dWidth; int dHeight; float mPercent; private RecyclerView.Adapter mAdapter; public static final float CRITICAL_PERCENT = 0.8f; @Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public int getIntrinsicWidth() { return dWidth; } @Override public int getIntrinsicHeight() { return dHeight; } public void setPercent(float percent,boolean invalidate){ mPercent = percent; if (mAdapter != null && invalidate) mAdapter.notifyDataSetChanged(); } public float getPercent(){ return mPercent; } public void setAdapter(RecyclerView.Adapter adapter){ mAdapter = adapter; } /** * u have to initial ur bitmaps u needed , dWidth and dHeight here * * @param context context */ protected abstract void init(Context context); }
这是我们需要用到的Drawable对象,其中的percent和我们说的前景实现一样,是描述刷新(加载)完成度的一个度量值。setPercent中调用adapter的notifyDataSetChanged。里面的dWidth和dHeight必须被设置,否则drawable无法绘制。
这里面要说一下为什么把最小的高度值设为0而不是1,是因为我发现一个bug,如果高度设为0 ,那么下面这个方法会工作不正常。这个bug导致和LinearLayouManagert的逻辑和每16ms绘制一次界面的机制有关系。有兴趣的可以查看下android源码,我并没有进一步测试。
private boolean canChildScrollBottom() { return ViewCompat.canScrollVertically(this, 1); }所以我重写了这个方法。
private boolean canChildScrollBottom(){ return !showLoadFlag && !isLastChildShowingCompletely(); } private boolean isLastChildShowingCompletely(){ return ((getLayoutManager().getPosition(getChildAt(getChildCount() - 2)) == getAdapter().getItemCount() - 2)); }
public abstract class RefreshLoadWrapper extends HeaderAndFooterWrapper { private final static int REFRESH_TYPE = 199999; private final static int LOAD_TYPE = 199998; private boolean canRefresh = false; private boolean canLoad = false; private DrawableView mRefresh; private DrawableView mLoad; public RefreshLoadWrapper(Context context) { super(context); } private void addRefreshImage(Context c){ if (canRefresh){ deleteHeader(0,false); } mRefresh = new DrawableView(c); addHeader(0,REFRESH_TYPE); canRefresh = true; } public void setRefreshDrawable(Context context,AdvancedDrawable drawable){ addRefreshImage(context); mRefresh.setDrawable(drawable); } public void setLoadDrawable(Context context,AdvancedDrawable drawable){ addLoadImage(context); mLoad.setDrawable(drawable); } private void addLoadImage(Context c){ if (canLoad){ deleteFooter(getFooterViewCount() - 1,false); } mLoad = new DrawableView(c); addFooter(0,LOAD_TYPE); canLoad = true; } public void setRefreshHeight(int height){ mRefresh.setHeight(height); } public void setLoadHeight(int height){ mLoad.setHeight(height); } public abstract RecyclerView.ViewHolder onCreateHeaderVH(ViewGroup parent, int viewType); public abstract RecyclerView.ViewHolder onCreateFooterVH(ViewGroup parent, int viewType); public abstract RecyclerView.ViewHolder onCreateGeneralVH(ViewGroup parent, int viewType); public abstract void onBindHeaderVH(RecyclerView.ViewHolder holder, int position); public abstract void onBindFooterVH(RecyclerView.ViewHolder holder, int position); public abstract void onBindGeneralVH(RecyclerView.ViewHolder holder, int position); @Override public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType) { if (viewType == REFRESH_TYPE){ return new MyViewHolder(mRefresh); } return onCreateHeaderVH(parent, viewType); } @Override public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType) { if (viewType == LOAD_TYPE){ return new MyViewHolder(mLoad); } return onCreateFooterVH(parent, viewType); } @Override public RecyclerView.ViewHolder onCreateGeneralViewHolder(ViewGroup parent, int viewType) { return onCreateGeneralVH(parent,viewType); } @Override public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder, int position) { if (!(position == 0 && canRefresh)) { onBindHeaderVH(holder, position); } } @Override public void onBindFooterViewHolder(RecyclerView.ViewHolder holder, int position) { if (!(position == getFooterViewCount() - 1 && canLoad)){ onBindFooterVH(holder,position); } } @Override public void onBindGeneralViewHolder(RecyclerView.ViewHolder holder, int position) { onBindGeneralVH(holder, position); } }
这个是Adapter实现了setDrawable的时候先添加一个View为Header,这个View也是drawable的载体。
@Override public void setAdapter(Adapter adapter) { super.setAdapter(adapter); if (adapter instanceof RefreshLoadWrapper){ Log.d(TAG,"adapter kind of RefreshLoadWrapper"); expectedAdapter = true; ((RefreshLoadWrapper) adapter).setRefreshDrawable(getContext(),mRefreshDrawable); ((RefreshLoadWrapper) adapter).setLoadDrawable(getContext(),mLoadDrawable); mRefreshDrawable.setAdapter(adapter); mLoadDrawable.setAdapter(adapter); }else { expectedAdapter = false; } }
这是自定义的RecyclerView,重写了setAdatper,如果设置的adapter是支持刷新和加载更多的adapter那么就设置默认的drawable。
啊,刚吃完中秋晚饭,继续写。
下面我们要说说自定义的RecyclerView,这个自定义的RecyclerView和前景实现中自定义的RecyclerView的手势处理差不多,就是有一些细节需要处理。下面是整个
自定义的RecyclerView的代码
public class AdvancedDrawableRecyclerView extends RecyclerView {
private boolean canRefresh = true;
private boolean canLoad = false;
private static final int DRAG_MAX_DISTANCE_V = 300;
public static final long MAX_OFFSET_ANIMATION_DURATION = 500;
private static final float DRAG_RATE = 0.3f;
private float INITIAL_X = -1;
private float INITIAL_Y = -1;
private float lastY = 0;
private static final String TAG = "ADR";
private AdvancedDrawable mRefreshDrawable;
private AdvancedDrawable mLoadDrawable;
private boolean expectedAdapter = false;
private boolean showRefreshFlag = false;
private boolean showLoadFlag = false;
private ValueAnimator animator;
private Interpolator mInterpolator = new LinearInterpolator();
private RefreshableAndLoadable mDataSource;
private boolean gettingData = false;
public AdvancedDrawableRecyclerView(Context context) {
super(context);
init(context);
}
public AdvancedDrawableRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public AdvancedDrawableRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
void init(Context context){
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
mRefreshDrawable = new SunAdvancedDrawable(context,this);
mLoadDrawable = new SunAdvancedBottomDrawable(context,this);
}
public void setRefreshDrawable(AdvancedDrawable drawable){
mRefreshDrawable = drawable;
if (expectedAdapter){
((RefreshLoadWrapper) getAdapter()).setRefreshDrawable(getContext(),mRefreshDrawable);
}
}
public void setLoadDrawable(AdvancedDrawable drawable){
mLoadDrawable = drawable;
if (expectedAdapter){
((RefreshLoadWrapper) getAdapter()).setRefreshDrawable(getContext(),mLoadDrawable);
}
}
@Override public void setAdapter(Adapter adapter) { super.setAdapter(adapter); if (adapter instanceof RefreshLoadWrapper){ Log.d(TAG,"adapter kind of RefreshLoadWrapper"); expectedAdapter = true; ((RefreshLoadWrapper) adapter).setRefreshDrawable(getContext(),mRefreshDrawable); ((RefreshLoadWrapper) adapter).setLoadDrawable(getContext(),mLoadDrawable); mRefreshDrawable.setAdapter(adapter); mLoadDrawable.setAdapter(adapter); }else { expectedAdapter = false; } }
@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
if (!expectedAdapter || (!canRefresh && !canLoad))return super.onTouchEvent(ev);
if (gettingData)return true;
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
if (isRunning()){//
// can stop animation
stop();
//fix initial action_down position
calculateInitY(MotionEventCompat.getY(ev,0),DRAG_MAX_DISTANCE_V,DRAG_RATE,
showRefreshFlag ? mRefreshDrawable.getPercent() : -mLoadDrawable.getPercent());
}else {
INITIAL_Y = MotionEventCompat.getY(ev,0);
lastY = INITIAL_Y;
}
break;
case MotionEvent.ACTION_MOVE:
final float agentY = MotionEventCompat.getY(ev,0);
if (agentY > INITIAL_Y){
// towards bottom
if (!canChildScrollUp()){
if (!canRefresh)return super.onTouchEvent(ev);
if (showLoadFlag)showLoadFlag = false;
if (!showRefreshFlag){
showRefreshFlag = true;
INITIAL_Y = agentY;
}
mRefreshDrawable.setPercent(fixPercent(Math.abs(calculatePercent(INITIAL_Y,
agentY,DRAG_MAX_DISTANCE_V,DRAG_RATE))),true);
((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(mRefreshDrawable.getPercent()));
lastY = agentY;
return true;
}else {
if(showRefreshFlag)showRefreshFlag = false;
lastY = agentY;
break;
}
} else if (agentY < INITIAL_Y){
if (!canChildScrollBottom()){
if (!canLoad)return super.onTouchEvent(ev);
if (showRefreshFlag)showRefreshFlag = false;
if (!showLoadFlag){
showLoadFlag = true;
INITIAL_Y = agentY;
lastY = agentY;
}
if (lastY == agentY){
break;
}
float prePercent = mLoadDrawable.getPercent();
float newPercent = fixPercent(Math.abs(calculatePercent(INITIAL_Y,
agentY, DRAG_MAX_DISTANCE_V, DRAG_RATE)));
mLoadDrawable.setPercent(newPercent,true);
((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(newPercent)); getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(newPercent)));
lastY = agentY;
return true;
}else {
if (showLoadFlag)showLoadFlag = false;
lastY = agentY;
break;
}
}else {
showLoadFlag = showRefreshFlag = false;
mRefreshDrawable.setPercent(0,false);
mLoadDrawable.setPercent(0,true);
lastY = agentY;
return true;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (!showLoadFlag && !showRefreshFlag)break;
actionUpOrCancel();
return true;
}
return super.onTouchEvent(ev);
}
private boolean isRunning(){
return animator != null && (animator.isRunning() || animator.isStarted());
}
private void stop(){
animator.cancel();
}
private int getViewOffset(float percent){
if (showRefreshFlag){
return Math.min((int) (percent * (float) mRefreshDrawable.getIntrinsicHeight() * 0.8),
mRefreshDrawable.getIntrinsicHeight());
}
return Math.min((int) (percent * (float) mLoadDrawable.getIntrinsicHeight() * 0.8),
mRefreshDrawable.getIntrinsicHeight());
}
private void actionUpOrCancel(){ if(showLoadFlag && showRefreshFlag){ throw new IllegalStateException("load state and refresh state should be mutual exclusion!"); } if (showRefreshFlag){ if (mRefreshDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){ // 回到临界位置 toCriticalPositionAnimation(mRefreshDrawable.getPercent()); }else { toStartPositionAnimation(mRefreshDrawable.getPercent()); } }else { if (mLoadDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){ // 回到临界位置 toCriticalPositionAnimation(mLoadDrawable.getPercent()); }else { toStartPositionAnimation(mLoadDrawable.getPercent()); } } }
private void toCriticalPositionAnimation(final float start){ animator = ValueAnimator.ofFloat(start,AdvancedDrawable.CRITICAL_PERCENT); animator.setInterpolator(mInterpolator); animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * (start - AdvancedDrawable.CRITICAL_PERCENT))); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float percent = (float) animation.getAnimatedValue(); if (showRefreshFlag){ mRefreshDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent)); }else { float prePercent = mLoadDrawable.getPercent(); mLoadDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent)); getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent))); } if (percent == AdvancedDrawable.CRITICAL_PERCENT){ if (showRefreshFlag){ gettingData = true; Toast.makeText(getContext(),"refresh",Toast.LENGTH_SHORT).show(); if (mDataSource != null){ mDataSource.onRefreshing(); } mRefreshDrawable.start(); }else { gettingData = true; Toast.makeText(getContext(),"load",Toast.LENGTH_SHORT).show(); if (mDataSource != null){ mDataSource.onLoading(); } mLoadDrawable.start(); } } } }); animator.start(); }
private void toStartPositionAnimation(final float start){ animator = ValueAnimator.ofFloat(start,0); animator.setInterpolator(mInterpolator); animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * start)); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float percent = (float) animation.getAnimatedValue(); if (showRefreshFlag){ mRefreshDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent)); }else { float prePercent = mLoadDrawable.getPercent(); mLoadDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent)); getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent))); } if (percent == 0){ showLoadFlag = showRefreshFlag = false; } } }); animator.start(); }
private float fixPercent(float initPercent){
if (initPercent <= 1){
return initPercent;
}else {
return 1f + (initPercent - 1f) * 0.6f;
}
}
private float calculatePercent(float initialPos,float currentPos,int maxDragDistance,float rate){
return (currentPos - initialPos) * rate / ((float) maxDragDistance);
}
private void calculateInitY(float agentY,int maxDragDistance,float rate,float percent){
INITIAL_Y = agentY - percent * (float) maxDragDistance / rate;
}
private boolean canChildScrollUp() {
return ViewCompat.canScrollVertically(this, -1);
}
private boolean canChildScrollBottom(){ return !showLoadFlag && !isLastChildShowingCompletely(); } private boolean isLastChildShowingCompletely(){ return ((getLayoutManager().getPosition(getChildAt(getChildCount() - 2)) == getAdapter().getItemCount() - 2)); }
public void setRefreshableAndLoadable(RefreshableAndLoadable dataSource){
mDataSource = dataSource;
}
public void stopRefreshingOrLoading(){
if (gettingData){
gettingData = false;
}
if (showRefreshFlag){
mRefreshDrawable.stop();
toStartPositionAnimation(AdvancedDrawable.CRITICAL_PERCENT);
}else {
mLoadDrawable.stop();
toStartPositionAnimation(AdvancedDrawable.CRITICAL_PERCENT);
}
}
public void setCanRefresh(boolean canRefresh){
this.canRefresh = canRefresh;
}
public void setCanLoad(boolean canLoad){
this.canLoad = canLoad;
}
}
AdvancedDrawableRecyclerView这个类的onTouchEvent中有一个细节,就是处理加载更多时的手势代码中,有这两句
((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(newPercent)); getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(newPercent)));第一句是修改footer的高度,第二句是通知LayoutManager需要向下偏移一掉段距离,这个距离是新旧高度之差。那为啥要告诉LayoutManager要偏移呢?这是因为
LayoutManager代理RecyclerView的layout过程,而这里有一个被称为锚的类,这个类可以标记RecyclerView正在展示的child从哪开始,而child在上拉的过程中,如果不偏移,重绘的时候这个起始点的信息不会变,那么上面列表的高度没有变化,那么列表就不会随着手指向上移动,虽然footer高度变化了,但是看不出来,所以需要偏移去修正。
手势处理过程如下,和前景实现保持一致。
手势监听
|
| 如果如果手势向下,view不能向下继续滑动
|
计算滑动的距离,当滑动距离没有到阀值的时候,根据滑动距离和
最大滑动距离的百分比,计算绘制的图案位置,圆弧的角度等等
| |
| 当滑动时未超过阀值松开 | 超过阀值时松开
| |
这个时候让图案回弹就Ok 图案先回弹到阀值对应的位置,然后开始旋转,
当刷新完成的时候,回弹
手势过程中平移的百分比计算代码中不难理解,另外在前景实现中已经说过了,所以这儿不说了,可以参考前一篇文章。
我们说说手指抬起来后发生了什么
private void actionUpOrCancel(){ if(showLoadFlag && showRefreshFlag){ throw new IllegalStateException("load state and refresh state should be mutual exclusion!"); } if (showRefreshFlag){ if (mRefreshDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){ // 回到临界位置 toCriticalPositionAnimation(mRefreshDrawable.getPercent()); }else { toStartPositionAnimation(mRefreshDrawable.getPercent()); } }else { if (mLoadDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){ // 回到临界位置 toCriticalPositionAnimation(mLoadDrawable.getPercent()); }else { toStartPositionAnimation(mLoadDrawable.getPercent()); } } }
很简单,如果超过开始刷新(加载更多)的临界值,就开启回到临界位置的动画,否则就开启回到初始位置的动画
private void toCriticalPositionAnimation(final float start){ animator = ValueAnimator.ofFloat(start,AdvancedDrawable.CRITICAL_PERCENT); animator.setInterpolator(mInterpolator); animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * (start - AdvancedDrawable.CRITICAL_PERCENT))); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float percent = (float) animation.getAnimatedValue(); if (showRefreshFlag){ mRefreshDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent)); }else { float prePercent = mLoadDrawable.getPercent(); mLoadDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent)); getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent))); } if (percent == AdvancedDrawable.CRITICAL_PERCENT){ if (showRefreshFlag){ gettingData = true; Toast.makeText(getContext(),"refresh",Toast.LENGTH_SHORT).show(); if (mDataSource != null){ mDataSource.onRefreshing(); } mRefreshDrawable.start(); }else { gettingData = true; Toast.makeText(getContext(),"load",Toast.LENGTH_SHORT).show(); if (mDataSource != null){ mDataSource.onLoading(); } mLoadDrawable.start(); } } } }); animator.start(); }
这是回弹到临界位置的动画逻辑,这里面,需要更新时的计算出来的值就是Percent,然后把percent传递给Drawable,并且修改Drawable对应的view的高度。
回到初始位置的动画逻辑类似
private void toStartPositionAnimation(final float start){ animator = ValueAnimator.ofFloat(start,0); animator.setInterpolator(mInterpolator); animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * start)); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float percent = (float) animation.getAnimatedValue(); if (showRefreshFlag){ mRefreshDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent)); }else { float prePercent = mLoadDrawable.getPercent(); mLoadDrawable.setPercent(percent,true); ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent)); getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent))); } if (percent == 0){ showLoadFlag = showRefreshFlag = false; } } }); animator.start(); }
我们继续看看drawable是如何根据percent值绘制的。
这是我自定义的一个drawable,对应的是默认的刷新的动画。
public class SunAdvancedDrawable extends AdvancedDrawable { private Bitmap mSky; private Bitmap mSun; private Matrix mMatrix; private static final long MAX_OFFSET_ANIMATION_DURATION = 1000; private ValueAnimator valueAnimator; private Interpolator mInterpolator = new LinearInterpolator(); private int mSkyHeight; private int mSunSize = 100; private float mSunLeftOffset = 220; private float mRotate = 0.0f; private int sunRoutingHeight; private boolean startAnimation = false; public SunAdvancedDrawable(Context context, final View view){ super(); view.post(new Runnable() { @Override public void run() { dWidth = view.getMeasuredWidth(); dHeight = (int) (dWidth * 0.5f); mMatrix = new Matrix(); init(view.getContext()); } }); } @Override protected void init(Context context){ final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; mSky = BitmapFactory.decodeResource(context.getResources(), R.drawable.sky, options); mSky = Bitmap.createScaledBitmap(mSky, dWidth, dHeight, true); mSkyHeight = dHeight; sunRoutingHeight = (int) ((mSkyHeight - mSunSize) * 0.9); mSunLeftOffset = 0.3f * (float) dWidth; createBitmaps(context); } private void createBitmaps(Context context) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; mSky = BitmapFactory.decodeResource(context.getResources(), R.drawable.sky, options); mSky = Bitmap.createScaledBitmap(mSky, dWidth, mSkyHeight, true); mSun = BitmapFactory.decodeResource(context.getResources(), R.drawable.sun, options); mSun = Bitmap.createScaledBitmap(mSun, mSunSize, mSunSize, true); } @Override public void start() { startAnimation = true; ensureAnimation(); valueAnimator.start(); } @Override public void stop() { startAnimation = false; if(valueAnimator.isRunning() || valueAnimator.isStarted()){ valueAnimator.cancel(); } } @Override public boolean isRunning() { return valueAnimator != null && valueAnimator.isRunning(); } @Override public void draw(@NonNull Canvas canvas) { drawSky(canvas); drawSun(canvas); } private void ensureAnimation(){ valueAnimator = ValueAnimator.ofFloat(0,359); valueAnimator.setDuration(MAX_OFFSET_ANIMATION_DURATION); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.setRepeatMode(ValueAnimator.RESTART); valueAnimator.setInterpolator(mInterpolator); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mRotate = (float) animation.getAnimatedValue(); invalidateSelf(); } }); } private void drawSky(Canvas canvas) { Matrix matrix = mMatrix; matrix.reset(); int offsetY = (int) (30 * Math.min(mPercent,1)) - 50; matrix.postTranslate(0,offsetY); canvas.drawBitmap(mSky, matrix, null); } private void drawSun(Canvas canvas) { Matrix matrix = mMatrix; matrix.reset(); float dragPercent = mPercent; if (dragPercent > 1){ dragPercent = 1f + (dragPercent - 1f) * 0.4f; } int offsetY = (int) (Math.max(mSkyHeight - mSunSize - (int) (dragPercent * sunRoutingHeight),0) * 0.8); matrix.postTranslate(mSunLeftOffset,offsetY); matrix.postRotate( startAnimation ? mRotate : 360 * mPercent, mSunLeftOffset + mSunSize / 2, offsetY + mSunSize / 2); canvas.drawBitmap(mSun, matrix, null); } }
这是根据github上优秀项目pullToRefresh改写的,在这里感谢pullToRefresh项目作者。
我们继续说源码,自定义的drawable中根据传进来的View初始化了相关数据。
draw的过程分为两步,绘制天空,绘制太阳。
private void drawSky(Canvas canvas) { Matrix matrix = mMatrix; matrix.reset(); int offsetY = (int) (30 * Math.min(mPercent,1)) - 50; matrix.postTranslate(0,offsetY); canvas.drawBitmap(mSky, matrix, null); }绘制天空中offsetY的作用时让天空有一个向下平移的过程,而不是呆板的保持不动。
private void drawSun(Canvas canvas) { Matrix matrix = mMatrix; matrix.reset(); float dragPercent = mPercent; if (dragPercent > 1){ dragPercent = 1f + (dragPercent - 1f) * 0.4f; } int offsetY = (int) (Math.max(mSkyHeight - mSunSize - (int) (dragPercent * sunRoutingHeight),0) * 0.8); matrix.postTranslate(mSunLeftOffset,offsetY); matrix.postRotate( startAnimation ? mRotate : 360 * mPercent, mSunLeftOffset + mSunSize / 2, offsetY + mSunSize / 2); canvas.drawBitmap(mSun, matrix, null); }绘制太阳逻辑除了高度的偏移值,还需要计算旋转的角度。
关于刷新时太阳旋转的动画实现
private void ensureAnimation(){ valueAnimator = ValueAnimator.ofFloat(0,359); valueAnimator.setDuration(MAX_OFFSET_ANIMATION_DURATION); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.setRepeatMode(ValueAnimator.RESTART); valueAnimator.setInterpolator(mInterpolator); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mRotate = (float) animation.getAnimatedValue(); invalidateSelf(); } }); }
开启一个ValueAnimator,这个ValueAnimator用来计算每个时间点对应的角度值。这样随着时间的流逝,太阳就转动了。
如果你想自定义Drawable,你只需要创建一个继承AdvancedDrawable的类,实现相关方法,然后把类实例设置给Adapter就好。
好了,这个项目差不多就是这样了,欢迎各位留言交流。
欢迎加入github优秀项目分享群:589284497,不管你是项目作者或者爱好者,请来和我们一起交流吧。
相关文章推荐
- 高性能的给RecyclerView添加下拉刷新和加载更多动画,基于ItemDecoration(一)
- 基于RecyclerView的封装,仿qq侧拉删除效果,实现下拉刷新,上拉加载更多,添加header,添加footer
- RecyclerView展示固定数据、上拉加载更多、下拉刷新、点击事件、长按点击事件、删除条目、刷新条目、添加条目、多条目加载
- RecyclerView 轻松实现下拉刷新,上拉加载更多,添加HeaderView
- RecyclerView添加下拉刷新和上拉加载更多
- RecyclerView 添加下拉刷新和上拉加载更多
- RecyclerView的下拉刷新和加载更多 动画
- RecyclerView封装--添加下拉刷新和上拉加载更多
- RecyclerView的下拉刷新和加载更多 动画
- RecyclerView系列之:添加下拉刷新和上拉加载更多
- RecyclerView系列之(3):添加下拉刷新和上拉加载更多
- RecyclerView系列之(3):添加下拉刷新和上拉加载更多
- RecyclerView+SwipeRefreshLayout+ViewPager实现上拉加载更多下拉刷新和添加Banner(附源码)
- Android RecyclerView+item动画+下拉刷新,上拉加载更多,侧滑删除(易用可定制)
- 【Android】给RecyclerView添加下拉刷新和加载更多(二)
- XRecyclerView:实现下拉刷新、滚动到底部加载更多以及添加header功能的RecyclerView
- recyclerview的数据刷新(下拉刷新和自动加载更多)以及添加提示语(例如:“数据已加载完毕”)
- RefreshRecyclerView下拉刷新,加载更多
- Android LRecyclerView实现下拉刷新,滑动到底部自动加载更多
- Android RecyclerView下拉刷新、上拉加载更多、到底自动加载更多