Android自訂View實現鐘擺效果進度條PendulumView_Android

來源:互聯網
上載者:User

在網上看到了一個IOS組件PendulumView,實現了鐘擺的動畫效果。由於原生的進度條確實是不好看,所以想可以自訂View實現這樣的效果,以後也可以用於載入頁面的進度條。 

廢話不多說,先上效果圖

 

底部黑邊是錄製時不小心錄上的,可以忽略。 

既然是自訂View我們就按標準的流程來,第一步,自訂屬性 

自訂屬性 

建立屬性檔案 

在Android項目的res->values目錄下建立一個attrs.xml檔案,檔案內容如下:

 <?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="PendulumView">  <attr name="globeNum" format="integer"/>  <attr name="globeColor" format="color"/>  <attr name="globeRadius" format="dimension"/>  <attr name="swingRadius" format="dimension"/> </declare-styleable></resources>

其中declare-styleable的name屬性用於在代碼中引用該屬性檔案。name屬性,一般情況下寫的都是我們自訂View的類名,較為直觀。

使用styleale,系統可以為我們完成很多常量(int[]數組,下標常量)等的編寫,簡化我們的開發工作,例如下面代碼中用到的R.styleable.PendulumView_golbeNum等就是系統為我們自動產生的。 

globeNum屬性工作表示小球數量,globeColor表示小球顏色,globeRadius表示小球半徑,swingRadius表示擺動半徑 

讀取屬性值 

在自定view的構造方法中通過TypedArray讀取屬性值 

通過AttributeSet同樣可以擷取屬性值,但是如果屬性值是參考型別,則得到的只是ID,仍需繼續通過解析ID擷取真正的屬性值,而TypedArray直接協助我們完成了上述工作。 

public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    //使用TypedArray讀取自訂的屬性值    TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView);    int count = ta.getIndexCount();    for (int i = 0; i < count; i++) {      int attr = ta.getIndex(i);      switch (attr) {        case R.styleable.PendulumView_globeNum:          mGlobeNum = ta.getInt(attr, 5);          break;        case R.styleable.PendulumView_globeRadius:          mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));          break;        case R.styleable.PendulumView_globeColor:          mGlobeColor = ta.getColor(attr, Color.BLUE);          break;        case R.styleable.PendulumView_swingRadius:          mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));          break;      }    }    ta.recycle(); //避免下次讀取時出現問題    mPaint = new Paint();    mPaint.setColor(mGlobeColor);  }

重寫OnMeasure()方法 

@Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int widthMode = MeasureSpec.getMode(widthMeasureSpec);    int widthSize = MeasureSpec.getSize(widthMeasureSpec);    int heightMode = MeasureSpec.getMode(heightMeasureSpec);    int heightSize = MeasureSpec.getSize(heightMeasureSpec);    //高度為小球半徑+擺動半徑    int height = mGlobeRadius + mSwingRadius;    //寬度為2*擺動半徑+(小球數量-1)*小球直徑    int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;    //如果測量模式為EXACTLY,則直接使用推薦值,如不為EXACTLY(一般處理wrap_content情況),使用自己計算的寬高    setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height);  }

其中
 int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
用於處理測量模式為AT_MOST的情況,一般是自訂View的寬高設定為了wrap_content,此時通過小球的數量,半徑,擺動的半徑等計算View的寬高,如下圖: 

以小球個數5為例,View的大小為下圖紅色矩形地區 

重寫onDraw()方法 

@Override  protected void onDraw(Canvas canvas) {    super.onDraw(canvas);    //繪製除左右兩個小球外的其他小球    for (int i = 0; i < mGlobeNum - 2; i++) {      canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint);    }    if (mLeftPoint == null || mRightPoint == null) {      //初始化最左右兩小球座標      mLeftPoint = new Point(mSwingRadius, mSwingRadius);      mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius);      //開啟擺動動畫      startPendulumAnimation();    }    //繪製左右兩小球    canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint);    canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint);  }

onDraw()方法是自訂View的關鍵所在,在該方法體內繪製View的顯示效果。代碼首先繪製了除去最左邊最右邊小球以外的其他小球,然後對左右兩小球的座標值進行判斷,如果是第一次繪製,座標值均為空白,則初始化兩小球座標,並且開啟動畫。最後通過mLeftPoint,mRightPoint的x,y值,繪製左右兩個小球。 

其中mLeftPoint,mRightPoint均是android.graphics.Point對象,僅是使用它們來存放左右兩小球的x,y座標資訊。 

使用屬性動畫 

public void startPendulumAnimation() {    //使用屬性動畫    final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {      @Override      public Object evaluate(float fraction, Object startValue, Object endValue) {        //參數fraction用於表示動畫的完成度,我們根據它來計算當前的動畫值        double angle = Math.toRadians(90 * fraction);        int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));        int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));        Point point = new Point(x, y);        return point;      }    }, new Point(), new Point());    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {      @Override      public void onAnimationUpdate(ValueAnimator animation) {        Point point = (Point) animation.getAnimatedValue();        //獲得當前的fraction值        float fraction = anim.getAnimatedFraction();        //判斷是否是fraction先減小後增大,即是否處於即將向上擺動狀態        //在每次即將向上擺動時切換小球        if (lastSlope && fraction > mLastFraction) {          isNext = !isNext;        }        //通過不斷改動左右小球的x,y座標值實現動畫效果        //利用isNext來判斷應該是左邊小球動,還是右邊小球動        if (isNext) {          //當左邊小球擺動時,右邊小球置於初始位置          mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1);          mRightPoint.y = mSwingRadius;          mLeftPoint.x = mSwingRadius - point.x;          mLeftPoint.y = mGlobeRadius + point.y;        } else {          //當右邊小球擺動時,左邊小球置於初始位置          mLeftPoint.x = mSwingRadius;          mRightPoint.y = mSwingRadius;          mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x;          mRightPoint.y = mGlobeRadius + point.y;        }        invalidate();        lastSlope = fraction < mLastFraction;        mLastFraction = fraction;      }    });    //設定永久迴圈播放    anim.setRepeatCount(ValueAnimator.INFINITE);    //設定迴圈模式為倒序播放    anim.setRepeatMode(ValueAnimator.REVERSE);    anim.setDuration(200);    //設定補間器,控制動畫的變化速率    anim.setInterpolator(new DecelerateInterpolator());    anim.start();  }

 其中使用ValueAnimator.ofObject方法是為了可以對Point對象進行操作,更為形象具體。還有就是通過ofObject方法使用了自訂的TypeEvaluator對象,由此得到了fraction值,該值是一個從0-1變化的小數。所以該方法的後兩個參數startValue(new Point()),endValue(new Point())並沒有實際意義,也可以直接不寫,此處寫上主要是為了便於理解。同樣道理也可以直接使用ValueAnimator.ofFloat(0f, 1f)方法擷取到一個從0-1變化的小數。

     final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {      @Override      public Object evaluate(float fraction, Object startValue, Object endValue) {        //參數fraction用於表示動畫的完成度,我們根據它來計算當前的動畫值        double angle = Math.toRadians(90 * fraction);        int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));        int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));        Point point = new Point(x, y);        return point;      }    }, new Point(), new Point());

通過fraction,我們計算得到小球擺動時的角度變化值,0-90度

 

mSwingRadius-mGlobeRadius表示的值是圖中綠色直線的長度,擺動的路線,小球圓心的路線是一個以(mSwingRadius-mGlobeRadius)為半徑的弧線,變化的X值為(mSwingRadius-mGlobeRadius)*sin(angle),變化的y值為(mSwingRadius-mGlobeRadius)*cos(angle) 

對應的小球實際的圓心座標為(mSwingRadius-x,mGlobeRadius+y) 

右邊小球運動路線與左邊類似,僅僅是方向不同。右邊小球實際的圓心座標(mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x,mGlobeRadius+y) 

可見左右兩邊小球的縱座標是相同的,僅橫座標不同。 

        float fraction = anim.getAnimatedFraction();        //判斷是否是fraction先減小後增大,即是否處於即將向上擺動狀態        //在每次即將向上擺動時切換小球        if (lastSlope && fraction > mLastFraction) {          isNext = !isNext;        }        //記錄上一次fraction是否不斷減小        lastSlope = fraction < mLastFraction;        //記錄上一次的fraction        mLastFraction = fraction;

 這兩段代碼用於計算何時切換運動的小球,本動畫設定了迴圈播放,且迴圈模式為倒序播放,所以動畫的一個周期即為小球拋起加上小球落下的過程。在該過程中fraction的值先有0變為1,再由1變為0。那麼何時是動畫新一輪周期的開始呢?就是在小球即將拋起的時候,在這個時候切換運動的小球,即可實現左邊小球落下後右邊小球拋起,右邊小球落下後左邊小球拋起的動畫效果。 

那麼如何捕捉到這個時間點呢? 

小球拋起時fraction值不斷增大,小球落下時fraction值不斷減小。小球即將拋起的時刻,就是fraction從不斷減小轉變為不斷增大的時刻。代碼中記錄上一次fraction是否在不斷減小,然後比較這一次fraction是否在不斷增大,若兩個條件均成立則切換運動的小球。 

    anim.setDuration(200);    //設定補間器,控制動畫的變化速率    anim.setInterpolator(new DecelerateInterpolator());    anim.start();

設定動畫的期間為200毫秒,讀者可以通過更改該值而達到修改小球擺動速度的目的。

設定動畫的補間器,由於小球拋起是一個逐漸減速的過程,落下是一個逐漸加速的過程,所以使用DecelerateInterpolator實現減速效果,在倒序播放時為加速效果。 

啟動動畫,鐘擺效果的自訂View進度條就實現了!趕快運行,看看效果吧!

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援雲棲社區。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.