AndroidカスタムView時計振子効果進捗バーPendulumView


インターネットで見たIOSユニットのPendulumViewは、振り子のアニメーション効果を実現しました。オリジナルのプログレスバーは確かに綺麗ではないので、Viewをカスタマイズしてこのような効果を実現したいです。今後はページのプログレスバーをロードするのにも使えます。 
余計なことを言わないで、先に効果図を書きます。
 
下の黒い端は録音時に不注意で録音しました。無視できます。 
カスタムViewなら標準的な流れで、第一歩、カスタム属性 
カスタム属性 
属性ファイルを作成 
Androidプロジェクトのres->valuesディレクトリの下にatrs.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-styleのname属性は、コード内でこの属性ファイルを参照するために使用されます。name属性は、一般的に私たちがカスタマイズしたViewの類名で、より直感的です。
styleを使って、システムは私達のために多くの定数(int[配列],標準量)などの編纂を完成することができます。私達の開発作業を簡略化します。例えば、下記のコードで使うR.style.PendulumView。golbeNumなどはシステムが私たちのために自動的に生成します。 
globeNum属性は小さいボールの数を表し、globeColorは小さいボールの色を表し、globeRadiusは小さいボールの半径を表し、swingRadiusはスイング半径を表します。 
属性値を読み出す 
自定viewの構造方法でTypedArayで属性値を読み出す 
AttributeSetでも属性値を取得できますが、属性値が参照タイプであれば、IDだけを取得しても、解析IDで真の属性値を取得し続ける必要があります。TypedArayは直接に上記の作業を完了しました。 

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);
  }

書き換え方法 


@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オブジェクトであり、それらを使って左右の2つのボールの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();
  }

 この中でValue Animator.objectを使う方法はPointオブジェクトを操作できるように、より具体的にイメージするためです。また、ObjectによってカスタムType Everalatorオブジェクトを使用したことによりfract値が得られ、この値は0-1から変化する小数点である。したがって、この方法の後の二つのパラメータstartValue(new Point)は、endValue(new Point)は、実際的な意味がなく、直接書かなくてもいいです。ここには、理解しやすいように書いてあります。同様に、Value Animator.ofloat(0 f,1 f)方法を使って、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は図中の緑色の直線の長さを表しています。スイングのコースは、小さいボールの円心のコースは半径の弧であり、変化のX値は(mSwingRadius-mGloberadius)*sin(angggggladius)であり、変化の値はRadiusです。 
対応するボールの実際の中心座標は(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ミリ秒に設定します。読者はこの値を変更することで、小球のスイング速度を変更することができます。
アニメーションの補間器を設置して、小さいボールが投げ始めるのは1つの次第に減速する過程なため、落ちるのは1つの次第に加速する過程で、だからDecell erate Interpolatorを使って減速の効果を実現して、倒順が放送する時加速する効果です。 
動画を起動すると、時計の振り子効果をカスタマイズしたViewの進捗バーが実現します。早く運行して効果を見てください。
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。