您的位置:首页 > 移动开发 > Android开发

高性能的给RecyclerView添加下拉刷新和加载更多动画,基于drawable(二)

2017-10-04 14:43 423 查看
项目已经上传github,点击这里查看

先看看效果





动画很粗糙,请不要在意。

项目是基于我改进的一个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,不管你是项目作者或者爱好者,请来和我们一起交流吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐