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

Analyse détaillée de l'animation des propriétés de l'original Android

Préambule

Dans le développement quotidien, les animations sont incontournables, et les animations de propriétés sont encore plus puissantes. Nous devons non seulement savoir comment les utiliser, mais aussi comprendre leur principe. De cette manière, nous pourrons les manier avec aisance. Alors, aujourd'hui, parlons de ce qui est le plus simple, pour comprendre le principe des animations de propriétés.

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

ObjectAnimator#ofInt

Prenez cet exemple, le code est le suivant.

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

Dans cette méthode, tout d'abord, un objet ObjectAnimator est créé, puis les valeurs sont insérées via la méthode setIntValues, puis retournées. Dans le constructeur d'ObjectAnimator, l'objet cible de l'animation est configuré via la méthode setTarget, et le nom de l'attribut courant est configuré via la méthode setPropertyName. Nous allons nous concentrer sur la méthode setIntValues.

public void setIntValues(int... values) {}}
 if (mValues == null || mValues.length == 0) {
 // Pas encore de valeurs - cet animateur est construit par morceaux. Initialisez les valeurs avec
 // quelle que soit la propriété propertyName actuelle
 if (mProperty != null) {
 setValues(PropertyValuesHolder.ofInt(mProperty, values));
 } else {
 setValues(PropertyValuesHolder.ofInt(mPropertyName, values));
 }
 } else {
 super.setIntValues(values);
 }
}

Tout d'abord, il faut vérifier si mValues est null, ici c'est null, et mProperty est également null, donc on appelle
La méthode setValues(PropertyValuesHolder.ofInt(mPropertyName, values));. Regardons d'abord la méthode PropertyValuesHolder.ofInt, la classe PropertyValuesHolder conserve les attributs et les valeurs. Dans cette méthode, un objet IntPropertyValuesHolder est construit et renvoyé.

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

Voici le constructeur de IntPropertyValuesHolder :

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

Ici, on appelle d'abord le constructeur de la catégorie, puis la méthode setIntValues. Dans le constructeur de la classe parente, seul propertyName est configuré. Le contenu de setIntValues est le suivant :

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

Dans la méthode setIntValues du parent, mValueType est initialisé à int.class, mKeyframes à KeyframeSet.ofInt(values). KeyframeSet est une collection de cadres clés. Ensuite, mKeyframes est assigné à mIntKeyframes.

KeyframeSet

Cette classe enregistre les cadres clés. Regardons sa méthode 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]);
 }
 }
 retournez new IntKeyframeSet(keyframes);
}

ici ? Selon les valeurs transmises, calculez les cadres clés et retournez finalement IntKeyframeSet.

retourner à ObjectAnimator, ici setValues utilise la méthode de la classe mère 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);
 }
 // New property/values/the target should cause re-initialization before starting
 mInitialized = false;
}

The operation here is simple, just put the PropertyValuesHolder into the mValuesMap.

ObjectAnimator#start

This method is the starting point of the animation.

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, "Cible d'animation, durée: " + 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();
}

D'abord, il récupère l'objet AnimationHandler, s'il n'est pas vide, il juge s'il s'agit d'une animation de mAnimations, mPendingAnimations ou mDelayedAnims, et l'annule. Enfin, il appelle la méthode start de la classe parente.

ValueAnimator#start

private void start(boolean playBackwards) {
 if (Looper.myLooper() == null) {
 throw new AndroidRuntimeException("Les animateurs ne peuvent être exécutés que sur les threads Looper");
 }
 mReversing = playBackwards;
 mPlayingBackwards = playBackwards;
 if (playBackwards && mSeekFraction != -1) {
 if (mSeekFraction == 0 && mCurrentIteration == 0) {
 // cas spécial : revenir à partir d'une recherche-à-0 devrait agir comme si aucun recherche n'avait été effectuée
 mSeekFraction = 0;
 } else if (mRepeatCount == INFINITE) {
 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)) {
 // si nous avons été recherchés à une autre itération dans un animateur inversé,
 // décider de la direction correcte pour commencer à jouer en fonction de l'itération
 if (playBackwards) {
 mPlayingBackwards = (mCurrentIteration % 2) == 0;
 } else {
 mPlayingBackwards = (mCurrentIteration % 2) != 0;
 }
 }
 int prevPlayingState = mPlayingState;
 mPlayingState = STOPPED;
 mStarted = true;
 mStartedDelay = false;
 mPaused = false;
 updateScaledDuration(); // au cas où le facteur de mise à l'échelle a changé depuis le temps de création
 AnimationHandler animationHandler = getOrCreateAnimationHandler();
 animationHandler.mPendingAnimations.add(this);
 if (mStartDelay == 0) {
 // Cela définit la valeur initiale de l'animation, avant de la démarrer
 if (prevPlayingState != SEEKED) {
 setCurrentPlayTime(0);
 }
 mPlayingState = STOPPED;
 mRunning = true;
 notifyStartListeners();
 }
 animationHandler.start();
}
  • Initialiser d'abord certaines valeurs
  • updateScaledDuration ajuster la durée, par défaut c'est1.0f
  • Obtenir ou créer AnimationHandler, ajouter l'animation à la liste mPendingAnimations,
  • Si il n'y a pas de délai, informer les listeners
  • animationHandler.start

Dans animationHandler.start, la méthode scheduleAnimation est appelée, dans ce cas, mChoreographerpost un callback, qui exécutera finalement la méthode run de mAnimate. mChoreographerpost implique VSYNC, n'en parlons pas davantage.

mAnimate#run

doAnimationFrame(mChoreographer.getFrameTime());

Ici, nous utiliserons doAnimationFrame pour configurer les frames d'animation, regardons le code de cette méthode.

void doAnimationFrame(long frameTime) {
 mLastFrameTime = frameTime;
 // mPendingAnimations contient toutes les animations qui ont demandé à être démarrées
 // Nous allons vider mPendingAnimations, mais l'animation de départ peut
 // causer plus à la liste en attente (par exemple, si une animation
 // un déclenchement supplémentaire). Donc nous bouclons jusqu'à ce que mPendingAnimations
 // est vide.
 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);
 // Si l'animation a un startDelay, placez-la sur la liste différée
 if (anim.mStartDelay == 0) {
 anim.startAnimation(this);
 } else {
 mDelayedAnims.add(anim);
 }
 }
 }
 // Ensuite, traitez les animations actuellement en file d'attente différée, ajoutant
 // les ajouter aux animations actives s'ils sont prêts
 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();
 }
 // Traitez maintenant toutes les animations actives. La valeur de retour de animationFrame()
 // informe le gestionnaire s'il doit maintenant être terminé
 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();
 }
 // Planifiez le committal final pour le cadre.
 mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, mCommit, null);
 // Si il y a encore des animations actives ou différées, planifiez un appel futur à
 // onAnimate pour traiter le prochain cadre des animations.
 if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
 scheduleAnimation();
 }
}

La méthode est longue, et la logique est la suivante :

  1. Prendre l'animation de la liste mPendingAnimations, et selon le choix préalable de startAnimation ou d'ajouter à la liste mDelayedAnims.
  2. Si les animations de la liste mDelayedAnims sont prêtes, les ajouter à la liste mReadyAnims
  3. Prendre l'animation à exécuter de la liste mAnimations, et l'ajouter à la liste mTmpAnimations
  4. Exécuter l'animation de trame par la méthode doAnimationFrame
  5. Continuer l'exécution de scheduleAnimation

De ce qui précède, nous pouvons voir que la clé de l'exécution de l'animation est la méthode doAnimationFrame. Dans cette méthode, la méthode animationFrame est appelée.

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) {
 // Passer au début
 mCurrentIteration = mRepeatCount;
 if (!mReversing) {
  mPlayingBackwards = false;
 }
 }
 if (fraction >= 1f) {
 if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
  // Il est temps de répéter
  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;
  // Note: We do not need to update the value of mStartTimeCommitted here
  // since we just added a duration offset.
 } else {
  done = true;
  fraction = Math.min(fraction, 1.0f);
 }
 }
 if (mPlayingBackwards) {
 fraction = 1f - fraction;
 }
 animateValue(fraction);
 break;
 }
 return done;
 }
  • Calculate fraction
  • Call the animateValue method

According to the principle of dynamic dispatch by the virtual machine execution engine, here it will call the animateValue method of ObjectAnimator.

ObjectAnimator#animateValue

void animateValue(float fraction) {
 final Object target = getTarget();
 if (mTarget != null && target == null) {
 // We lost the target reference, cancel and clean up.
 cancel();
 return;
 }
 super.animateValue(fraction);
 int numValues = mValues.length;
 for (int i = 0; i < numValues; ++i) {
 mValues[i].setAnimatedValue(target);
 }
}

Here are mainly two things to do,

  1. Call the animateValue method of the superclass
  2. Through setAnimatedValue to set properties

Son class method as follows:

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

Dans cette méthode, nous obtenons le fraction actuel à l'aide de l'Interpolator et calculons la valeur actuelle à l'aide de calculateValue. Ici, nous appelons calculateValue de IntPropertyValuesHolder

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

Nous savons que mIntKeyframes correspond à IntKeyframeSet. Dans la méthode getIntValue de cette classe, la valeur correspondante actuelle est calculée à l'aide de TypeEvaluator. Plus de détails.

Enfin, revenons à animateValue. Après avoir calculé la valeur, nous appelons setAnimatedValue pour définir la valeur. Regardons son implémentation.

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

Oui, ici vous pouvez voir les traces de modification des valeurs des propriétés, il y a les quatre cas suivants

  1. mIntProperty n'est pas null
  2. mProperty n'est pas null
  3. mJniSetter n'est pas null
  4. mSetter n'est pas null

Tout d'abord, nous construisons un objet en utilisant les paramètres String propertyName, int... values, mIntProperty est null, et mProperty est également null. Alors comment les deux autres sont-ils venus ? Il semble que quelque chose manque ?

Pourquoi appeler directement startAnimation dans doAnimationFrame ?63;C'est bien ici.

startAnimation

Dans cette méthode, la méthode initAnimation est appelée. Selon les règles de dispatchage dynamique, la méthode initAnimation d'ObjectAnimator est appelée ici. Ici, la méthode setupSetterAndGetter de PropertyValuesHolder est appelée, ici l'initialisation de mSetter est effectuée, etc., je ne vais pas en dire plus, vous pouvez voir le code vous-même.

C'est tout pour les animations de propriétés dans Android. J'espère que le contenu de cet article peut aider les développeurs Android. Si vous avez des questions, vous pouvez laisser des commentaires pour échanger. Merci de votre soutien au tutoriel d'alarme.

Déclaration : Le contenu de cet article est issu du réseau, propriété de ses auteurs respectifs, apporté par les utilisateurs d'Internet et téléchargé spontanément. Ce site ne détient pas de droits de propriété, n'a pas été édité par l'homme, et n'assume aucune responsabilité juridique connexe. Si vous trouvez du contenu présumé enfreindre les droits d'auteur, veuillez envoyer un email à : notice#oldtoolbag.com (veuillez remplacer # par @ lors de l'envoi d'un email pour signaler une violation, et fournir des preuves pertinentes. Une fois vérifié, ce site supprimera immédiatement le contenu présumé enfreindre les droits d'auteur.)

Vous pourriez aussi aimer