English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

AndroidでパーソナライズされたViewを実現するピンとぶる効果のプログレスバーPendulumView

ネットで見つけた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の場合は、推奨値を使用します。それ以外の場合(一般的には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()メソッドはカスタムビューの鍵となります。このメソッドの内部でビューの表示効果を描画します。コードはまず最も左端および右端のボールを除いた他のボールを描画し、次に左右のボールの座標値を判断します。初めて描画する場合、座標値は空であるため、両ボールの座標を初期化し、アニメーションを開始します。最後に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変化する小数を取得します。したがって、このメソッドの後2つの引数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が減少しているかどうかを記録し、今回のfractionが増加しているかどうかを比較し、両方の条件が成立した場合には動きを変更するボールを切り替えます。 

    anim.setDuration(200);
    //補間器を設定し、アニメーションの変化速度を制御します
    anim.setInterpolator(new DecelerateInterpolator());
    anim.start();

アニメーションの持続時間を設定します200ミリ秒、読者はこの値を変更することでボールの揺れの速度を変更することができます。

アニメーションの補間器を設定します。ボールが投げ上げられるのは減速する過程であり、落ちるのは加速する過程であるため、DecelerateInterpolatorを使用して減速効果を実現し、逆再生時には加速効果を提供します。 

アニメーションを起動し、ペンデュラム効果のカスタムViewプロセスバーが実現しました!すぐに実行して、効果を見てください!

これで本文のすべての内容が終わります。皆様の学習に役立つことを願っています。また、呐喊教程を多くのサポートをお願いします。

声明:本文の内容はインターネットから取得しており、著作権者に帰属します。インターネットユーザーが自発的に貢献し、自己でアップロードしたものであり、本サイトは所有権を持ちません。また、人工的に編集はされていません。著作権侵害が疑われる内容がある場合は、以下のメールアドレスにご連絡ください:notice#oldtoolbag.com(メールを送信する際には、#を@に変更してください。侵害を報告する場合は、関連する証拠を提供し、一旦確認されると、本サイトは直ちに侵害される疑われる内容を削除します。)

おすすめ