高效能的給RecyclerView新增下拉重新整理和載入更多動畫,基於ItemDecoration(一)
專案已經上傳Github,點選這裡檢視,裡面有整個專案的效果。
先說說我為什麼要做這個。github上有一個比較火的開源專案,PullToRefreshView,估計不少人都看過,這個專案實現的動畫很漂亮,但是它有一個我無法忍受的缺點,就是當列表可以向下滾動的時候,你點選螢幕,然後手指向下移動,列表跟著向下滑,當列表滑動到頂部的時候,手指繼續向下移動的時候,重新整理的動畫效果並不會出現,只有你手指擡起再點擊向下滑動,重新整理的動畫才會出現。這個效果的出現是因為作者是自定義了一個VIewGroup,而非繼承ListView或者RecyclerView,原作者把重新整理對應的滑動手勢放在ViewGroup處理,而列表向下滑動時對應的手勢是在ListView或者RecyclerView中處理的,而當列表拿到action_down事件並處理,那麼後續的move,up之類的事件都會由它處理,這就會出現上述所說的問題。
從專案介紹裡大家可以看到,有兩種實現方式,這兩種效果都有很廣泛的應用.
第一種是前景實現,通過RecyclerView的內部類ItemDecoration實現,關於這個類網上資料很多,我就不展開講了,它主要起到一個裝飾的作用,既可以給每一個item裝飾,也可以裝飾整個RecyclerView,它內部的onDrawOver方法實現給整個RecyclerView新增裝飾。而前景動畫的旋轉效果正好可以看成是View的裝飾。
先說一個概念,滑動效果有一個最大距離,而手指移動的距離乘以一個係數得到的距離是圖案在螢幕上移動的距離,這個距離和最大距離的比值是計算圖案展示效果的關鍵係數,當這個值大於0.8的時候,我們就認為可以重新整理,如果小於這個值就認為還不能下拉重新整理。
前景的實現邏輯,說說下拉重新整理,載入更多原理類似。
手勢監聽
|
| 如果如果手勢向下,view不能向下繼續滑動
|
計算滑動的距離,當滑動距離沒有到閥值的時候,根據滑動距離和
最大滑動距離的百分比,計算繪製的圖案位置,圓弧的角度等等
| |
| 當滑動時未超過閥值鬆開 | 超過閥值時鬆開
| |
這個時候讓圖案回彈就Ok 圖案先回彈到閥值對應的位置,然後開始旋轉,
當重新整理完成的時候,回彈
整個邏輯就是這樣。
下面結合程式碼來說說。翠花,上程式碼。
public interface ItemDecorAnimatable extends Animatable{
/**
* coder maybe should call this method before call {@link #interruptAnimation()}
*
* @return true if can interrupt animation ,otherwise return false
*/
boolean canInterrupt();
/**
* ItemDecor's animation maybe be complicated
* when new TouchEvent received but ItemDecor still animating,if ItemDecor support interrupt
*u should override this method
*/
void interruptAnimation();
}
首先從架構的角度考慮下,一個前景效果需要定義哪些方法。它需要實現動畫效果,那麼它要實現Animatable介面中的方法吧。另外是不是這個介面就滿足使用了?
我們再仔細想想這樣一個需求,當我把手指向下移動到沒有達到閥值的時候擡起手指的時候,圖案應該是回彈的,如果說回彈需要0.5秒,但是我擡起0.3秒後就立刻按下去,那麼此時圖案的回彈動畫還沒有結束的,那麼這個後續的action_down怎麼處理?偷懶的話直接無視,等到回彈動畫結束再響應新的介面。這麼處理可以,但是我想做的更使用者友好呢?那麼我應該把動畫打斷,並且使圖案停留在打斷時對應的位置上。再想想,如果是正在重新整理時,手指點選,是不應該打斷重新整理過程的,這個時候不應該打斷動畫,所以我新定義了ItemDecorAnimatable介面,它繼承了Animatable同時定義了兩個方法:
boolean canInterrupt();
判斷是否可以中斷動畫
void interruptAnimation();
中斷動畫
這兩個動畫是配合使用的,你在呼叫interruptAnimation前應該先呼叫canInterrupt判斷是否應該呼叫。
public abstract class CustomItemDecor extends RecyclerView.ItemDecoration implements ItemDecorAnimatable {
protected float refreshPercent = 0.0f;
protected float loadPercent = 0.0f;
private View mParent;
protected RefreshableAndLoadable mDataSource;
public CustomItemDecor(View parent){
mParent = parent;
}
public void setRefreshPercent(float percent) {
if (percent == refreshPercent)return;
refreshPercent = percent;
mParent.invalidate();
}
public void setLoadPercent(float percent) {
if (percent == loadPercent)return;
loadPercent = percent;
mParent.invalidate();
}
public float getRefreshPercent() {
return refreshPercent;
}
public float getLoadPercent() {
return loadPercent;
}
public void setRefreshableAndLoadable(RefreshableAndLoadable dataSource){
mDataSource = dataSource;
}
}
這是繼承了ItemDecoration類和ItemDecorAnimatable介面的抽象類,目的是給自實現的ItemDecoration定義幾個必須的元素。
refreshPercent是重新整理動畫百分比,loadPercent是載入更多百分比。mParent是ItemDecoration所在的RecyclerView。
public class AdvancedRecyclerView extends RecyclerView
AdvancedRecyclerView是自定義RecyclerView
private boolean canRefresh = true;
private boolean canLoad = false;
private static final int DRAG_MAX_DISTANCE_V = 300;
private static final float DRAG_RATE = 0.48f;
public static final long MAX_OFFSET_ANIMATION_DURATION = 500;
private float INITIAL_Y = -1;
private Interpolator mInterpolator = new LinearInterpolator();
private static final String TAG = "AdvancedRecyclerView";
private boolean showRefreshFlag = false;
private boolean showLoadFlag = false;
private CustomItemDecor mItemDecor;
這是AdvancedRecyclerView中的一些屬性。都是字面意思。
我們看看此類中onTouchEvent方法的實現
@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
if ((!canRefresh && !canLoad) || !mItemDecor.canInterrupt())return super.onTouchEvent(ev);
final int action = MotionEventCompat.getActionMasked(ev);
switch (action){
case MotionEvent.ACTION_DOWN:
if (!mItemDecor.isRunning()){
INITIAL_Y = MotionEventCompat.getY(ev,0);
}else {// animating
if(!mItemDecor.canInterrupt())return super.onTouchEvent(ev);
mItemDecor.interruptAnimation();
// 如果取消正在執行的動畫,需要動用calculateInitY方法把對應的INITIAL_Y計算出來
// 由於RecyclerView記錄的action down的位置和我們邏輯上的action down位置不一致
// 所以要手動生成一個MotionEvent物件作為引數呼叫super.onTouchEvent()來修正
calculateInitY(MotionEventCompat.getY(ev,0),DRAG_MAX_DISTANCE_V,DRAG_RATE,
showRefreshFlag ? mItemDecor.getRefreshPercent() : -mItemDecor.getLoadPercent());
// correct action-down position
final MotionEvent simulateEvent = MotionEvent.obtain(ev);
simulateEvent.offsetLocation(0, INITIAL_Y - MotionEventCompat.getY(ev,0));
return super.onTouchEvent(simulateEvent);
}
break;
case MotionEvent.ACTION_MOVE:
final float agentY = MotionEventCompat.getY(ev,0);
if (agentY > INITIAL_Y){
// towards bottom
if (showLoadFlag)showLoadFlag = false;// 手指上下襬動導致狀態切換
if (canChildScrollUp()){
if(showRefreshFlag){
showRefreshFlag = false;
mItemDecor.setRefreshPercent(0);
}else {
return super.onTouchEvent(ev);
}
break;
}else {// 不能向下滾動
if (!canRefresh)return super.onTouchEvent(ev);
if (!showRefreshFlag){// 從能滾動切換為不能滾動
showRefreshFlag = true;
INITIAL_Y = agentY;
}
mItemDecor.setRefreshPercent(fixPercent(calculatePercent(INITIAL_Y,agentY,DRAG_MAX_DISTANCE_V,DRAG_RATE)));
}
}else if(agentY < INITIAL_Y) {
// towards top
if (showRefreshFlag)showRefreshFlag = false;// 手指上下襬動導致狀態切換
if(canChildScrollBottom()){
if(showLoadFlag){
showLoadFlag = false;
mItemDecor.setLoadPercent(0);
}else {
return super.onTouchEvent(ev);
}
break;
}else {
if (!canLoad)return super.onTouchEvent(ev);
if(!showLoadFlag){// 從能滾動切換為不能滾動
showLoadFlag = true;
INITIAL_Y = agentY;
}
mItemDecor.setLoadPercent(fixPercent(Math.abs(calculatePercent(INITIAL_Y,agentY,DRAG_MAX_DISTANCE_V,DRAG_RATE))));
}
}else {
clearState();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
actionUpOrCancel();
return super.onTouchEvent(ev);
case MotionEventCompat.ACTION_POINTER_UP:
break;
}
return true;
}
來按手勢流程一點點剖析這個方法。
首先接收到action_down的時候,如果當前沒有動畫的時候,直接記錄下手指在螢幕Y軸的位置就Ok。
手指在螢幕上隨意滑動,agentY是手指move時Y軸的位置,如果agentY如果大於INIT_Y,那麼手指就是向下滑動,canChildScrollUp判斷能否頂部繼續滑動(列表向下),如果不能,就設定下拉重新整理的狀態,把下拉重新整理的標識showRefreshFlag設為true。這時設定INIT_Y為agentY,這時的INIT_Y是對應真正開始顯示動畫的值。接下來就是計算動畫的百分比了,這個百分比用來控制整個動畫。
mItemDecor.setRefreshPercent(fixPercent(calculatePercent(INITIAL_Y,agentY,DRAG_MAX_DISTANCE_V,DRAG_RATE)));
回到action_down的時候,如果當前已經有正在的執行的動畫但是當前可以中斷動畫時應該怎麼處理?程式碼裡的中文註釋已經詳細解釋了為什麼要重新計算INIT_Y值。已經執行的動畫使用的是上一次手勢過程中計算出來的INIT_Y,而再次收到action_down時,如果想讓當前動畫停留在當前位置,那麼就必須把action_down手勢的Y軸的值當作一個普通move動作時的值計算出邏輯上對應的INIT_Y值,否則整個動畫會混亂掉。
計算的邏輯
/**
* 計算中斷動畫時,percent對應的InitY
* calculate the InitY corresponding to current(animation interrupted) percent
*
* @param agentY
* @param maxDragDistance
* @param rate
* @param percent
*/
private void calculateInitY(float agentY,int maxDragDistance,float rate,float percent){
INITIAL_Y = agentY - percent * (float) maxDragDistance / rate;
}
/**
* 計算百分比 coder可以調節rate值來改變手指移動的距離改變percent的速度
* @param initialPos
* @param currentPos
* @param maxDragDistance
* @param rate
* @return
*/
private float calculatePercent(float initialPos,float currentPos,int maxDragDistance,float rate){
return (currentPos - initialPos) * rate / ((float) maxDragDistance);
}
/**
* 當percent大於1時,大幅減少percent增長的幅度
* @param initPercent
* @return
*/
private float fixPercent(float initPercent){
if (initPercent <= 1){
return initPercent;
}else {
return 1f + (initPercent - 1f) * 0.6f;
}
}
計算百分比時把移動的差值乘以一個係數,是讓動畫的流程更自然點,coder可以修改這個係數控制手指滑動時更改動畫的速度。
計算百分比的函式和計算percent對應的INIT_Y的函式可以看成互為逆函式。
計算出來的百分比會設定給ItemDecoration。
看看ItemDecoration的實現。
public class ItemDecor extends CustomItemDecor {
Paint mPaint;
Paint ovalPaint;
final float backgroundRadius = 60;
final float ovalRadius = 41;
RectF oval;
final float START_ANGLE_MAX_OFFSET = 90;
final float SWEEP_ANGLE_MAX_VALUE = 300;
final float ovalWidth = (float) (ovalRadius / 3.8);
private ValueAnimator valueAnimator;
private ValueAnimator animator;
private float offsetAngle = 0;
private boolean canStopAnimation = true;
private static final float CRITICAL_PERCENT = 0.8f;// 可以refresh或load的臨界百分比
public ItemDecor(View view) {
super(view);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(getResources().getColor(R.color.colorWhite));
ovalPaint = new Paint();
ovalPaint.setColor(getResources().getColor(R.color.colorLightBlue));
ovalPaint.setStyle(Paint.Style.STROKE);
ovalPaint.setStrokeWidth(ovalWidth);
oval = new RectF();
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
if (showRefreshFlag && refreshPercent > 0){// refresh logo visible
// draw background circle
c.drawCircle(getMeasuredWidth() / 2, -backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius),
backgroundRadius, mPaint);
if (getSweepAngle() >= SWEEP_ANGLE_MAX_VALUE){// if need, draw circle point
drawCirclePoint(true,c);
}
calculateOvalAngle();
c.drawArc(oval,getStartAngle(),getSweepAngle(),false,ovalPaint);// draw arc
}else if (showLoadFlag && loadPercent > 0){// load logo visible
// draw background circle
c.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() + backgroundRadius -
loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius), backgroundRadius, mPaint);
if (getSweepAngle() >= SWEEP_ANGLE_MAX_VALUE){// if need, draw circle point
drawCirclePoint(false,c);
}
calculateOvalAngle();
c.drawArc(oval,getStartAngle(),getSweepAngle(),false,ovalPaint);// draw arc
}
}
/**
* draw circle point
*
* @param refresh if true draw refresh logo point, otherwise draw load logo point
* @param c canvas
*/
void drawCirclePoint(boolean refresh,Canvas c){
ovalPaint.setStyle(Paint.Style.FILL);
// calculate zhe angle of the point relative to logo central point
final double circleAngle = (360 - SWEEP_ANGLE_MAX_VALUE) / 2 - getStartAngle();
// calculate X coordinate for point center
final float circleX = getMeasuredWidth() / 2 + (float) (Math.cos(circleAngle * Math.PI / 180) * ovalRadius);
// calculate Y coordinate for point center
float circleY;
if (refresh){
circleY = -backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) -
(float) (Math.sin(circleAngle * Math.PI / 180) * ovalRadius);
}else {
circleY = getMeasuredHeight() + backgroundRadius -
loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) -
(float) (Math.sin(circleAngle * Math.PI / 180) * ovalRadius);
}
c.drawCircle(circleX,circleY, ovalWidth / 2 + 2,ovalPaint);
ovalPaint.setStyle(Paint.Style.STROKE);
}
/**
* calculate arc circumcircle's position
*/
private void calculateOvalAngle(){
if (showRefreshFlag){
oval.set(getMeasuredWidth() / 2 - ovalRadius,-backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) - ovalRadius,
getMeasuredWidth() / 2 + ovalRadius,-backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) + ovalRadius);
}else {
oval.set(getMeasuredWidth() / 2 - ovalRadius,getMeasuredHeight() + backgroundRadius - loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) - ovalRadius,
getMeasuredWidth() / 2 + ovalRadius,getMeasuredHeight() + backgroundRadius - loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) + ovalRadius);
}
}
/**
* calculate start angle if percent larger than CRITICAL_PERCENT start angle should offset a little relative to 0
*
* @return start angle
*/
private float getStartAngle(){
final float percent = showRefreshFlag ? refreshPercent : loadPercent;
if (percent <= CRITICAL_PERCENT){
return 0 + offsetAngle;
}
return START_ANGLE_MAX_OFFSET * (percent - CRITICAL_PERCENT) + offsetAngle;
}
/**
* calculate oval sweep angle
*
* @return sweep angle
*/
private float getSweepAngle(){
final float percent = showRefreshFlag ? refreshPercent : loadPercent;
if (percent > 0 && percent <= CRITICAL_PERCENT){
return percent / CRITICAL_PERCENT * SWEEP_ANGLE_MAX_VALUE;
}
return SWEEP_ANGLE_MAX_VALUE;
}
@Override
public void start() {
if (showLoadFlag && showRefreshFlag){
throw new IllegalStateException("load state and refresh state should be mutual exclusion!");
}
if (showRefreshFlag){
if (refreshPercent >= CRITICAL_PERCENT){
toCriticalPositionAnimation(refreshPercent);
initRotateAnimation();
}else {
translationAnimation(refreshPercent,0);
}
}else {
if (loadPercent >= CRITICAL_PERCENT){
toCriticalPositionAnimation(loadPercent);
initRotateAnimation();
}else {
translationAnimation(loadPercent,0);
}
}
}
@Override
public boolean isRunning() {
if (animator != null){
if(animator.isRunning() || animator.isStarted())return true;
}
if (valueAnimator != null){
if(valueAnimator.isRunning() || valueAnimator.isStarted())return true;
}
return false;
}
/**
* 讓標識平移到臨界位置
* @param start
*/
private void toCriticalPositionAnimation(final float start){
animator = ValueAnimator.ofFloat(start,CRITICAL_PERCENT);
animator.setInterpolator(mInterpolator);
animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * (start - CRITICAL_PERCENT)));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
if (showRefreshFlag){
setRefreshPercent(value);
}else {
setLoadPercent(value);
}
if (value == CRITICAL_PERCENT){
startRotateAnimation();
if (showRefreshFlag){
if (mDataSource != null){
mDataSource.onRefreshing();
}
}else {
if (mDataSource != null){
mDataSource.onLoading();
}
}
}
}
});
animator.start();
}
/**
* 讓標識平移到起始位置
* @param start
* @param end
*/
private void translationAnimation(final float start,final float end){
animator = ValueAnimator.ofFloat(start,end);
animator.setInterpolator(mInterpolator);
animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * Math.min(start,1)));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
if (showRefreshFlag){
setRefreshPercent(value);
}else {
setLoadPercent(value);
}
if (value == end){
showLoadFlag = showRefreshFlag = false;
}
}
});
animator.start();
}
/**
* 開始旋轉動畫
*/
void initRotateAnimation(){
valueAnimator = ValueAnimator.ofFloat(1,360);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setDuration(1100);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offsetAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
canStopAnimation = false;
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
offsetAngle = 0;
canStopAnimation = true;
if (showRefreshFlag){
translationAnimation(refreshPercent,0);
}else if(showLoadFlag) {
translationAnimation(loadPercent,0);
}
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
private void startRotateAnimation(){
valueAnimator.start();
}
@Override
public void stop(){
if (valueAnimator != null && (valueAnimator.isStarted() || valueAnimator.isRunning())){
valueAnimator.cancel();
}
}
@Override
public boolean canInterrupt(){
return canStopAnimation;
}
@Override
public void interruptAnimation(){
if (!canStopAnimation)return;
if (animator != null && (animator.isStarted() || animator.isRunning())){
animator.cancel();
}
}
}
先解釋一下屬性
backgroundRadius是載入動畫的白色背景圓的半徑
ovalRadius是藍色圓弧的半徑
START_ANGLE_MAX_OFFSET是圓弧開始繪製時的起始角度(這個角度會發生偏移)
SWEEP_ANGLE_MAX_VALUE圓弧的最大角度
ovalWidth圓弧寬度
offsetAngle這個值是為了旋轉動畫而存在的,它和START_ANGLE_MAX_OFFSET一起用於計算起始角度,這個
值不斷改變,使起始角度不斷改變,從而實現動畫效果
CRITICAL_PERCENT是判斷是否應該重新整理(載入)的臨界百分比值
ItemDecoration中的onDrawOver是實現繪製的方法
@Override
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
if (showRefreshFlag && refreshPercent > 0){// refresh logo visible
// draw background circle
c.drawCircle(getMeasuredWidth() / 2, -backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius),
backgroundRadius, mPaint);
if (getSweepAngle() >= SWEEP_ANGLE_MAX_VALUE){// if need, draw circle point
drawCirclePoint(true,c);
}
calculateOvalAngle();
c.drawArc(oval,getStartAngle(),getSweepAngle(),false,ovalPaint);// draw arc
}else if (showLoadFlag && loadPercent > 0){// load logo visible
// draw background circle
c.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() + backgroundRadius -
loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius), backgroundRadius, mPaint);
if (getSweepAngle() >= SWEEP_ANGLE_MAX_VALUE){// if need, draw circle point
drawCirclePoint(false,c);
}
calculateOvalAngle();
c.drawArc(oval,getStartAngle(),getSweepAngle(),false,ovalPaint);// draw arc
}
}
整個繪製邏輯比較簡單
繪製背景圓 ----- 繪製圓弧 ----- 如果有需要,繪製圓點(如果達到臨界值,就繪製,所以圓點也可以用
來判斷是否已經可以重新整理或載入)
以下的繪製過程我都是以重新整理動畫為例,載入更多的動畫類似。
c.drawCircle(getMeasuredWidth() / 2, -backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius),
backgroundRadius, mPaint);
繪製背景圓,可以看出起始Y值是-backgroudRadius,也是說圓開始是真好隱藏在螢幕正上方的,然後向下運動。/**
* draw circle point
*
* @param refresh if true draw refresh logo point, otherwise draw load logo point
* @param c canvas
*/
void drawCirclePoint(boolean refresh,Canvas c){
ovalPaint.setStyle(Paint.Style.FILL);
// calculate zhe angle of the point relative to logo central point
final double circleAngle = (360 - SWEEP_ANGLE_MAX_VALUE) / 2 - getStartAngle();
// calculate X coordinate for point center
final float circleX = getMeasuredWidth() / 2 + (float) (Math.cos(circleAngle * Math.PI / 180) * ovalRadius);
// calculate Y coordinate for point center
float circleY;
if (refresh){
circleY = -backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) -
(float) (Math.sin(circleAngle * Math.PI / 180) * ovalRadius);
}else {
circleY = getMeasuredHeight() + backgroundRadius -
loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) -
(float) (Math.sin(circleAngle * Math.PI / 180) * ovalRadius);
}
c.drawCircle(circleX,circleY, ovalWidth / 2 + 2,ovalPaint);
ovalPaint.setStyle(Paint.Style.STROKE);
}
如果有需要,繪製小圓點。計算圓點的圓心座標,需要計算circleAngle這個角度,你可以想像,
在背景圓圓心和我們要繪製的小圓點的圓心之間連一條線,而背景圓圓心也是一個座標系的圓點,
這個座標系X軸平行於android的X軸,這個角度就是連線和這個座標系X軸之間的夾角。
至於這個角度為什麼這麼算,這不畫圖不好說清啊,建議讀者拿一隻筆和一張紙自己畫畫看,幫助理解。
然後是繪製圓弧的邏輯
calculateOvalAngle();
c.drawArc(oval,getStartAngle(),getSweepAngle(),false,ovalPaint);// draw arc
android中,繪製圓弧是根據這個圓的外切矩形來決定位置的(繪製圓弧外切矩形其實
就是正方形,如果不是正方形,那麼繪製的就是橢圓弧)繪製圓弧分為兩步,計算相關引數和繪製
/**
* calculate arc circumcircle's position
*/
private void calculateOvalAngle(){
if (showRefreshFlag){
oval.set(getMeasuredWidth() / 2 - ovalRadius,-backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) - ovalRadius,
getMeasuredWidth() / 2 + ovalRadius,-backgroundRadius + refreshPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) + ovalRadius);
}else {
oval.set(getMeasuredWidth() / 2 - ovalRadius,getMeasuredHeight() + backgroundRadius - loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) - ovalRadius,
getMeasuredWidth() / 2 + ovalRadius,getMeasuredHeight() + backgroundRadius - loadPercent * (DRAG_MAX_DISTANCE_V + backgroundRadius) + ovalRadius);
}
}
這是計算外切圓的座標的,不難理解。
整個繪製邏輯到此講完了。
說下動畫邏輯
動畫分為兩種,一個是平移的動畫,和旋轉的動畫,我說的平移和旋轉非android中的平移和旋轉,而是都是通過
屬性動畫實現的。
/**
* 讓標識平移到臨界位置
* @param start
*/
private void toCriticalPositionAnimation(final float start){
animator = ValueAnimator.ofFloat(start,CRITICAL_PERCENT);
animator.setInterpolator(mInterpolator);
animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * (start - CRITICAL_PERCENT)));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
if (showRefreshFlag){
setRefreshPercent(value);
}else {
setLoadPercent(value);
}
if (value == CRITICAL_PERCENT){
startRotateAnimation();
if (showRefreshFlag){
if (mDataSource != null){
mDataSource.onRefreshing();
}
}else {
if (mDataSource != null){
mDataSource.onLoading();
}
}
}
}
});
animator.start();
}
這是從超過臨界的位置平移到臨界位置的動畫,平移完成後呼叫startRotateAnimation開始旋轉。
為什麼startRotateAnimation在onAnimationUpdate中呼叫而不是給動畫新增一個AnimatorLstener,
然後在監聽的onAnimationEnd中呼叫呢,這是因為這個動畫是可能被我們cancel掉的,只有真正開始旋轉才會掉用
重新整理的方法。而呼叫cancel也會讓onAnimationEnd方法執行。
這個在android原始碼中可以看到,我上一些原始碼,算是展開講下。
@Override
public void cancel() {
// Only cancel if the animation is actually running or has been started and is about
// to run
AnimationHandler handler = getOrCreateAnimationHandler();
if (mPlayingState != STOPPED
|| handler.mPendingAnimations.contains(this)
|| handler.mDelayedAnims.contains(this)) {
// Only notify listeners if the animator has actually started
if ((mStarted || mRunning) && mListeners != null) {
if (!mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
notifyStartListeners();
}
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
for (AnimatorListener listener : tmpListeners) {
listener.onAnimationCancel(this);
}
}
endAnimation(handler);
}
}
這是ValueAnimator中的cancel,最後呼叫了endAnimation,
protected void endAnimation(AnimationHandler handler) {
handler.mAnimations.remove(this);
handler.mPendingAnimations.remove(this);
handler.mDelayedAnims.remove(this);
mPlayingState = STOPPED;
mPaused = false;
if ((mStarted || mRunning) && mListeners != null) {
if (!mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
notifyStartListeners();
}
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
tmpListeners.get(i).onAnimationEnd(this);
}
}
mRunning = false;
mStarted = false;
mStartListenersCalled = false;
mPlayingBackwards = false;
mReversing = false;
mCurrentIteration = 0;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}
}
看到沒,把所有的監聽的onAnimationEnd方法執行了一遍。
回到我們專案的程式碼
/**
* 讓標識平移到起始位置
* @param start
* @param end
*/
private void translationAnimation(final float start,final float end){
animator = ValueAnimator.ofFloat(start,end);
animator.setInterpolator(mInterpolator);
animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * Math.min(start,1)));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
if (showRefreshFlag){
setRefreshPercent(value);
}else {
setLoadPercent(value);
}
if (value == end){
showLoadFlag = showRefreshFlag = false;
}
}
});
animator.start();
}
這是從某個位置平移到初始位置的動畫
/**
* 開始旋轉動畫
*/
void initRotateAnimation(){
valueAnimator = ValueAnimator.ofFloat(1,360);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setDuration(1100);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offsetAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
canStopAnimation = false;
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
offsetAngle = 0;
canStopAnimation = true;
if (showRefreshFlag){
translationAnimation(refreshPercent,0);
}else if(showLoadFlag) {
translationAnimation(loadPercent,0);
}
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
private void startRotateAnimation(){
valueAnimator.start();
}
這是旋轉動畫相關的兩個方法,邏輯很簡單。
好了,整個專案的主要結構說的差不多了,補充一些細節。
專案中的自定義ItemDecoration是放在RecylerView的內部的,這是因為ItemDecoration需要和RecyclerView
配合使用,而一些動畫需要的值放在RecyclerView中更合適,而ItemDecoration又需要用到這些值,如果把
ItemDecoration定義在外面,那就有需要定義新的介面傳遞這些值,這顯的很多餘,程式碼也不更容易理解,所以就沒
這麼做。
如果coder想實現自己的前景動畫,只需要在RecyclerView中新增一個內部類,這個類繼承CustomItemDecor,
再實現相關方法即可。
特別感謝github上的pullToRefresh的原作者。
動畫的背景實現我會寫一篇新的文章進行解析,後景是通過新增headerView和footerView配合Drawable實現。
最後,歡迎大家和我一起交流,如果有意見,可以直接留言討論。
歡迎加入github優秀專案分享群:589284497,不管你是專案作者或者愛好者,請來和我們一起交流吧。