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

Androidのソースコード解析の属性アニメーションの詳細

前書き

日常開発ではアニメーションは欠かせませんが、属性アニメーションはさらに強力です。私たちはただ使い方を知るだけでなく、その原理も知っておく必要があります。そうすることで、自由自在に使えるようになります。それでは、今日は最もシンプルなことから、属性アニメーションの原理について説明します。

ObjectAnimator
 .ofInt(mView,"width",100,500)
 .setDuration(1000)
 .start();

ObjectAnimator#ofInt

この例を元に、以下のコードです。

public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
 ObjectAnimator anim = new ObjectAnimator(target, propertyName);
 anim.setIntValues(values);
 anim;
}

このメソッドでは、まずObjectAnimatorオブジェクトをnewして、setIntValuesメソッドで値を設定し、それから返します。ObjectAnimatorのコンストラクタでは、setTargetメソッドで現在のアニメーションのオブジェクトを設定し、setPropertyNameで現在の属性名を設定します。ここで重点を置くのはsetIntValuesメソッドです。

public void setIntValues(int... values) {
 if (mValues == null || mValues.length == 0) {
 // まだ値はありません - このアニメーターは一部ずつ構築されています。値を以下で初期化します:
 // 現在のpropertyNameに関わらず
 if (mProperty != null) {
 setValues(PropertyValuesHolder.ofInt(mProperty, values));
 } else {
 setValues(PropertyValuesHolder.ofInt(mPropertyName, values));
 }
 } else {
 super.setIntValues(values);
 }
}

まずnullかどうかを判断します。ここではnullで、mPropertyもnullですので、以下を呼び出します:
setValues(PropertyValuesHolder.ofInt(mPropertyName, values));メソッドを見てみましょう。PropertyValuesHolderクラスは属性と値を保持しています。このメソッドでは、IntPropertyValuesHolderオブジェクトを構築し、返します。

public static PropertyValuesHolder ofInt(String propertyName, int... values) {
 return new IntPropertyValuesHolder(propertyName, values);
}

IntPropertyValuesHolderのコンストラクタは以下の通りです:

public IntPropertyValuesHolder(String propertyName, int... values) {
 super(propertyName);
 setIntValues(values);
}

ここでは、まずそのクラスのコンストラクタを呼び出し、setIntValuesメソッドを呼び出し、親クラスのコンストラクタではただpropertyNameを設定しています。setIntValuesの内容は以下の通りです:

public void setIntValues(int... values) {
 super.setIntValues(values);
 mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
}

親クラスのsetIntValuesメソッドでは、mValueTypeをint.class、mKeyframesをKeyframeSet.ofInt(values)に初期化し、その後KeyframeSetがキーフレーム集合として、mKeyframesをmIntKeyframesに割り当てます。

KeyframeSet

このクラスはキーフレームを記録するものです。そのofIntメソッドを見てみましょう。

public static KeyframeSet ofInt(int... values) {
 
 IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2);
 if (numKeyframes == 1) {
 keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
 keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
 } else {
 keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
 for (int i = 1; i < numKeyframes; ++i) {
 keyframes[i] =
  (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
 }
 }
 return new IntKeyframeSet(keyframes);
}

ここは何ですか?传入されたvaluesに基づいてキーフレームを計算し、最後にIntKeyframeSetを返します。

戻ってObjectAnimatorの中に入ります。ここではsetValuesは親クラスValueAnimatorの

ValueAnimator#setValues

public void setValues(PropertyValuesHolder... values) {
 int numValues = values.length;
 mValues = values;
 mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
 for (int i = 0; i < numValues; ++i) {
 PropertyValuesHolder valuesHolder = values[i];
 mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
 }
 // 新しいプロパティ/値/ターゲットが再-開始前の初期化
 mInitialized = false;
}

ここでの操作はシンプルで、PropertyValuesHolderをmValuesMapに格納します。

ObjectAnimator#start

このメソッドはアニメーション開始の場所です。

public void start() {
 // See if any of the current active/pending animators need to be canceled
 AnimationHandler handler = sAnimationHandler.get();
 if (handler != null) {
 int numAnims = handler.mAnimations.size();
 for (int i = numAnims - 1; i >= 0; i--) {
 if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
 ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
 if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
  anim.cancel();
 }
 }
 }
 numAnims = handler.mPendingAnimations.size();
 for (int i = numAnims - 1; i >= 0; i--) {
 if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
 ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
 if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
  anim.cancel();
 }
 }
 }
 numAnims = handler.mDelayedAnims.size();
 for (int i = numAnims - 1; i >= 0; i--) {
 if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
 ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
 if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
  anim.cancel();
 }
 }
 }
 }
 if (DBG) {
 Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
 for (int i = 0; i < mValues.length; ++i) {
 PropertyValuesHolder pvh = mValues[i];
 Log.d(LOG_TAG, " Values[" + i + "]: " +
 pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
 pvh.mKeyframes.getValue(1));
 }
 }
 super.start();
}

まず、AnimationHandlerオブジェクトを取得し、空でない場合は、mAnimations、mPendingAnimations、mDelayedAnimsのアニメーションを判定し、キャンセルします。最後に、親クラスのstartメソッドを呼び出します。

ValueAnimator#start

private void start(boolean playBackwards) {
 if (Looper.myLooper() == null) {
 throw new AndroidRuntimeException("アニメーターはLooperスレッド上でのみ実行できます");
 }
 mReversing = playBackwards;
 mPlayingBackwards = playBackwards;
 if (playBackwards && mSeekFraction != -1) {
 if (mSeekFraction == 0 && mCurrentIteration == 0) {
 // 特別なケース:seekからの逆方向-to-0はseekされないように行動するべき
 mSeekFraction = 0;
 }
 mSeekFraction = 1 - (mSeekFraction % 1);
 } else {
 mSeekFraction = 1 + mRepeatCount - (mCurrentIteration + mSeekFraction);
 }
 mCurrentIteration = (int) mSeekFraction;
 mSeekFraction = mSeekFraction % 1;
 }
 if (mCurrentIteration > 0 && mRepeatMode == REVERSE &&
 (mCurrentIteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
 // 逆方向のアニメーターに他のiterationにseekされた場合、
 // iterationに基づいて正しい再生方向を決定する
 if (playBackwards) {
 mPlayingBackwards = (mCurrentIteration % 2) == 0;
 } else {
 mPlayingBackwards = (mCurrentIteration % 2) != 0;
 }
 }
 int prevPlayingState = mPlayingState;
 mPlayingState = STOPPED;
 mStarted = true;
 mStartedDelay = false;
 mPaused = false;
 updateScaledDuration(); // 作成時からスケールファクターが変更された場合
 AnimationHandler animationHandler = getOrCreateAnimationHandler();
 animationHandler.mPendingAnimations.add(this);
 if (mStartDelay == 0) {
 // This sets the initial value of the animation, prior to actually starting it running
 if (prevPlayingState != SEEKED) {
 setCurrentPlayTime(0);
 }
 mPlayingState = STOPPED;
 mRunning = true;
 notifyStartListeners();
 }
 animationHandler.start();
}
  • 先初始化一些值
  • updateScaledDuration 缩放时间,默认为1.0f
  • 获取或创建AnimationHandler,将动画添加到mPendingAnimations列表中,
  • 如果没有延迟,通知监听器
  • animationHandler.start

在animationHandler.start中,会调用scheduleAnimation方法,在这个种,会用mChoreographerpost一个callback,最终会执行mAnimate的run方法。mChoreographerpost涉及到VSYNC,这里不多介绍。

mAnimate#run

doAnimationFrame(mChoreographer.getFrameTime());

在这里会用过doAnimationFrame设置动画帧,我们看下这个方法的代码。

void doAnimationFrame(long frameTime) {
 mLastFrameTime = frameTime;
 // mPendingAnimations包含任何请求开始播放的动画
 // 我们将清除mPendingAnimations,但开始动画可能会
 // 待处理列表中添加更多项(例如,如果有一个动画
 // 開始がもう一度開始をトリガーするため、mPendingAnimations}}
 // が空です。
 while (mPendingAnimations.size() > 0) {
 ArrayList<ValueAnimator> pendingCopy =
 (ArrayList<ValueAnimator>) mPendingAnimations.clone();
 mPendingAnimations.clear();
 int count = pendingCopy.size();
 for (int i = 0; i < count; ++i) {
 ValueAnimator anim = pendingCopy.get(i);
 // アニメーションにstartDelayがある場合、遅延リストに配置します
 if (anim.mStartDelay == 0) {
 anim.startAnimation(this);
 } else {
 mDelayedAnims.add(anim);
 }
 }
 }
 // 次に、遅延キューに座っているアニメーションを処理します、追加します
 // それらを準備完了のアニメーションに追加します
 int numDelayedAnims = mDelayedAnims.size();
 for (int i = 0; i < numDelayedAnims; ++i) {
 ValueAnimator anim = mDelayedAnims.get(i);
 if (anim.delayedAnimationFrame(frameTime)) {
 mReadyAnims.add(anim);
 }
 }
 int numReadyAnims = mReadyAnims.size();
 if (numReadyAnims > 0) {
 for (int i = 0; i < numReadyAnims; ++i) {
 ValueAnimator anim = mReadyAnims.get(i);
 anim.startAnimation(this);
 anim.mRunning = true;
 mDelayedAnims.remove(anim);
 }
 mReadyAnims.clear();
 }
 // Now process all active animations. The return value from animationFrame()
 // tells the handler whether it should now be ended
 int numAnims = mAnimations.size();
 for (int i = 0; i < numAnims; ++i) {
 mTmpAnimations.add(mAnimations.get(i));
 }
 for (int i = 0; i < numAnims; ++i) {
 ValueAnimator anim = mTmpAnimations.get(i);
 if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {
 mEndingAnims.add(anim);
 }
 }
 mTmpAnimations.clear();
 if (mEndingAnims.size() > 0) {
 for (int i = 0; i < mEndingAnims.size(); ++i) {
 mEndingAnims.get(i).endAnimation(this);
 }
 mEndingAnims.clear();
 }
 // Schedule final commit for the frame.
 mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, mCommit, null);
 // If there are still active or delayed animations, schedule a future call to
 // onAnimate to process the next frame of the animations.
 if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
 scheduleAnimation();
 }
}

The method is long, and the logic is as follows:}

  1. Take the animation from mPendingAnimations, and according to the previously selected startAnimation or add it to the mDelayedAnims list.
  2. If the animation in the mDelayedAnims list is ready, add it to the mReadyAnims list
  3. Take the animation to be executed from the mAnimations list and add it to the mTmpAnimations list
  4. Execute animation frames through the doAnimationFrame method
  5. Continue to execute scheduleAnimation

From above, we can see that the key to executing the animation is the doAnimationFrame method. In this method, the animationFrame method is called.

ValueAniator#animationFrame

boolean animationFrame(long currentTime) {
 boolean done = false;
 switch (mPlayingState) {
 case RUNNING:
 case SEEKED:
 float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
 if (mDuration == 0 && mRepeatCount != INFINITE) {
 // Skip to the end
 mCurrentIteration = mRepeatCount;
 if (!mReversing) {
  mPlayingBackwards = false;
 }
 }
 if (fraction >= 1f) {
 if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
  // Time to repeat
  if (mListeners != null) {
  int numListeners = mListeners.size();
  for (int i = 0; i < numListeners; ++i) {
  mListeners.get(i).onAnimationRepeat(this);
  }
  }
  if (mRepeatMode == REVERSE) {
  mPlayingBackwards = !mPlayingBackwards;
  }
  mCurrentIteration += (int) fraction;
  fraction = fraction % 1f;
  mStartTime += mDuration;
  // 注意:ここでmStartTimeCommittedの値を更新する必要はありません
  // 私たちが時間オフセットを追加したばかりであるため。
 } else {
  done = true;
  fraction = Math.min(fraction, 1.0f);
 }
 }
 if (mPlayingBackwards) {
 fraction = 1f - fraction;
 }
 animateValue(fraction);
 break;
 }
 return done;
 }
  • fractionの計算
  • animateValueメソッドを呼び出します

仮想機の実行エンジンによる動的分派原則に基づいて、ここではObjectAnimatorのanimateValueメソッドが呼び出されます。

ObjectAnimator#animateValue

void animateValue(float fraction) {
 最終的なObject target = getTarget();
 if (mTarget != null && target == null) {
 // ターゲットリファレンスを失ったため、キャンセルしてクリーンアップします。
 cancel();
 return;
 }
 super.animateValue(fraction);
 int numValues = mValues.length;
 for (int i = 0; i < numValues; ++i) {
 mValues[i].setAnimatedValue(target);
 }
}

ここでは主に二つのことを行っています、

  1. 親クラスのanimateValueメソッドを呼び出します
  2. 属性を設定するためにsetAnimatedValueを使用します

その親クラスのメソッドは以下の通りです:

void animateValue(float fraction) {
 fraction = mInterpolator.getInterpolation(fraction);
 mCurrentFraction = fraction;
 int numValues = mValues.length;
 for (int i = 0; i < numValues; ++i) {
 mValues[i].calculateValue(fraction);
 }
 if (mUpdateListeners != null) {
 int numListeners = mUpdateListeners.size();
 for (int i = 0; i < numListeners; ++i) {
 mUpdateListeners.get(i).onAnimationUpdate(this);
 }
 }
}

このメソッドでは、Interpolatorを通じて現在のfractionを取得し、calculateValueを通じて現在の値を計算します。ここではIntPropertyValuesHolderのcalculateValueが呼び出されます。

void calculateValue(float fraction) {
 mIntAnimatedValue = mIntKeyframes.getIntValue(fraction);
}

mIntKeyframesがIntKeyframeSetに対応していることを知っています。このクラスのgetIntValueメソッドでは、TypeEvaluatorを通じて現在の対応する値を計算します。これ以上詳しくは言いません。

最後に、animateValueに戻ります。値が計算された後、setAnimatedValueが呼び出され、値が設定されます。その実装を見てみましょう。

IntPropertyValuesHolder#setAnimatedValue

void setAnimatedValue(Object target) {
 if (mIntProperty != null) {
 mIntProperty.setValue(target, mIntAnimatedValue);
 return;
 }
 if (mProperty != null) {
 mProperty.set(target, mIntAnimatedValue);
 return;
 }
 if (mJniSetter != 0) {
 nCallIntMethod(target, mJniSetter, mIntAnimatedValue);
 return;
 }
 if (mSetter != null) {
 try {
 mTmpValueArray[0] = mIntAnimatedValue;
 mSetter.invoke(target, mTmpValueArray);
 catch (InvocationTargetException e) {
 Log.e("PropertyValuesHolder", e.toString());
 catch (IllegalAccessException e) {
 Log.e("PropertyValuesHolder", e.toString());
 }
 }
}

そうですね、ここでは属性値の変更が確認できます。以下の四つの状況があります

  1. mIntProperty が null ではありません
  2. mProperty が null ではありません
  3. mJniSetter が null ではありません
  4. mSetter が null ではありません

まず、String propertyName, int... values パラメータで構築されたオブジェクトでは、mIntProperty が null で、mProperty も null です。それでは、他の二つはどのようにして来たのでしょうか?何かが欠けてるように思えます。

doAnimationFrame では startAnimation を直接呼び出すのはどうでしょうか?63;その通りです。

startAnimation

このメソッドでは initAnimation メソッドが呼び出されています。動的分派規則に従って、ここで ObjectAnimator の initAnimation メソッドが呼び出されます。ここで PropertyValuesHolder の setupSetterAndGetter メソッドを呼び出し、ここで mSetter などの初期化が行われます。詳細は省略しますが、コードを見てください。

それでは、これで Android での属性アニメーションに関するすべての内容が終わりました。この記事の内容が、Android デベロッパーの方々に少しでも役立つことを願っています。何か疑問があれば、コメントを残してください。呐喊教程へのご支援に感謝します。

声明:この記事の内容はインターネットから取得しており、著作権者に帰属します。インターネットユーザーが自発的に貢献し、自己でアップロードしたものであり、このサイトは所有権を持ちません。人工編集は行われていません。著作権侵害を疑う内容があれば、お気軽にメール:notice#wまでご連絡ください。3codebox.com(メールを送信する際、#を@に変更してください)で通報し、関連する証拠を提供してください。一旦確認が取れましたら、このサイトは即座に侵害を疑われるコンテンツを削除します。

おすすめ