高性能的给RecyclerView添加下拉刷新和加载更多动画,基于drawable(二)
来源:互联网 发布:支付宝端口申请 编辑:程序博客网 时间:2024/06/03 14:41
项目已经上传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,不管你是项目作者或者爱好者,请来和我们一起交流吧。
- 高性能的给RecyclerView添加下拉刷新和加载更多动画,基于drawable(二)
- 高性能的给RecyclerView添加下拉刷新和加载更多动画,基于ItemDecoration(一)
- 【Android】给RecyclerView添加下拉刷新和加载更多(二)
- RecyclerView的下拉刷新和加载更多 动画
- RecyclerView 添加下拉刷新和上拉加载更多
- RecyclerView添加下拉刷新和上拉加载更多
- RecyclerView封装--添加下拉刷新和上拉加载更多
- 给RecyclerView最纯粹的下拉刷新和上拉加载更多
- 给RecyclerView最纯粹的下拉刷新和上拉加载更多
- RecyclerView下拉刷新和加载更多
- recyclerview下拉刷新和加载更多
- RecyclerView下拉刷新和加载更多
- 自定义的RecyclerView, 下拉刷新,加载更多.
- RecyclerView 使用总结(二):RecyclerView的下拉刷新、加载更多
- 添加头、尾和动画的下拉刷新RecyclerView
- RecyclerView+SwipeRefreshLayout+ViewPager实现上拉加载更多下拉刷新和添加Banner(附源码)
- RecyclerView系列之(3):添加下拉刷新和上拉加载更多
- RecyclerView系列之(3):添加下拉刷新和上拉加载更多
- STM32的位段操作
- 补充java基础面试题
- < 笔记 > Python
- Debian安装完成后的一些配置
- oracle 11g中文乱码
- 高性能的给RecyclerView添加下拉刷新和加载更多动画,基于drawable(二)
- Bootstrap(1)
- 【一】Java特征整理
- 机器学习-k近邻算法及kd树
- PTA 7-10(图) 旅游规划(25 分) 25分代码
- 页面之前跳转 和页面之间传递参数
- iOS NSMutableParagraphStyle
- python 手记3 〖笨方法学python习题18〗
- Bootstrap(2)