教你使用系统API来实现View与View转换动画

1,084 阅读6分钟

QQ20211126-145540.gif

一. 通过这篇文章你将学到以下内容

  • ThreadLocal的使用
  • 里氏替换原则
  • 自定义Drawable对象

主要涉及这三个主要类

Transition 动画容器的抽象类,一些变化延迟到子类去实现

TransitionManager 对外暴露动画的入口,提供开始动画和结束动画

MaterialContainerTransform 实现了Transition方法创建每个动画animator的对象

二. 如何使用

主要利用系统 TransitionManager.beginDelayedTransition();方法。

如果使用我封装的库 (库地址在文章结尾)

//当执行开始动画
transformationLayout.startTransform()
//当执行结束动画
transformationLayout.finishTransform()

三. 源码解析

3.1 [-> TransitionManager.java]

public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
    //sceneRoot 是执行动画view的父类
    //transition 是MaterialContainerTransform对象
    
    //如果Pending动画未添加,才会添加; 当前执行或是销毁时会被移除,保证只执行一次 【见小节3.4】
    if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
        if (Transition.DBG) {
            Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
                    sceneRoot + ", " + transition);
        }
        sPendingTransitions.add(sceneRoot);
        if (transition == null) {
            transition = sDefaultTransition;
        }
        final Transition transitionClone = transition.clone();
        sceneChangeSetup(sceneRoot, transitionClone);【见小节3.1.1】
        Scene.setCurrentScene(sceneRoot, null);
        //注册开始运行动画
        sceneChangeRunTransition(sceneRoot, transitionClone); 【见小节3.3】
    }
}

这里transition参数利用里氏替换原则,它是一个抽象类这里需要传递子类对象,来实现不同的特性。

3.1.1 [-> TransitionManager.java]

private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {

    // Capture current values
    ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
    //如果当前运行和要运行是同一个先暂停它,待可运行时一起执行
    if (runningTransitions != null && runningTransitions.size() > 0) {
        for (Transition runningTransition : runningTransitions) {
            runningTransition.pause(sceneRoot);
        }
    }
    
    //持久化一些动画的属性
    if (transition != null) {
        transition.captureValues(sceneRoot, true);
    }

    // Notify previous scene that it is being exited
    Scene previousScene = Scene.getCurrentScene(sceneRoot);
    if (previousScene != null) {
        previousScene.exit();
    }
}

3.1.2 [-> TransitionManager.java]


private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
        sRunningTransitions = new ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>();

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
    WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
            sRunningTransitions.get();
    //取出当前线程是否有正在运行的动画;如果没有创建当前线程的map容器
    if (runningTransitions != null) {
        ArrayMap<ViewGroup, ArrayList<Transition>> transitions = runningTransitions.get();
        if (transitions != null) {
            return transitions;
        }
    }
    ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
    runningTransitions = new WeakReference<>(transitions);
    sRunningTransitions.set(runningTransitions);
    return transitions;
}

sRunningTransitions是使用ThreadLocal来实现的;ThreadLocal可以实现多线程中数据不共享,从而解决线程安全问题。 ThreadLocal的set方法会把数据保存在当前线程的ThreadLocalMap字段上,每个线程都有自己的ThreadLocalMap,从而实现数据隔离效果。ThreadLocal和同步锁来比较,ThreadLocal通过每个线程处理自己的数据,从而增加内存,但速度快了,利用空间换时间。而同步锁解决多线程访问顺序,数据对象在内存中只有一份,内存小了,但访问速度相对长了。

3.2 [-> MaterialContainerTransform.java]

MaterialContainerTransform().apply {
    this.startView = startView //从哪个view开始转换;如果是结束动画一般是startView和endView取反来执行
    this.endView = endView
    this.pathMotion = this@TransformationLayout.pathMotion.getPathMotion() // 线性动画和曲线动画
    this.scrimColor = this@TransformationLayout.scrimColor //蒙版背景色
    this.drawingViewId = this@TransformationLayout.zOrder //动画容器的边界限制
    this.transitionDirection = this@TransformationLayout.direction.value  //
    this.fadeMode = this@TransformationLayout.fadeMode.value
    this.fitMode = MaterialContainerTransform.FIT_MODE_AUTO
    this.startElevation = this@TransformationLayout.ELEVATION_NOT_SET
    this.endElevation = this@TransformationLayout.ELEVATION_NOT_SET
    this.isElevationShadowEnabled = this@TransformationLayout.elevationShadowEnabled
    this.isHoldAtEndEnabled = this@TransformationLayout.isHoldAtEndEnabled
    duration = this@TransformationLayout.duration
    addTarget(endView)
    addListener(object : SimpleTransitionListener() {
        override fun onTransitionCancel(transition: Transition) {
            onFinishTransformation()
        }

        override fun onTransitionEnd(transition: Transition) {
            onFinishTransformation()
            onTransformFinishListener?.onTransformFinish(isTransformed)
        }
    })
}

3.3 [-> TransitionManager.java]

private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
        final Transition transition) {
    if (transition != null && sceneRoot != null) {
        //创建监听view绘制完成类
        MultiListener listener = new MultiListener(transition, sceneRoot);
        //监听view离开window我们做资源释放
        sceneRoot.addOnAttachStateChangeListener(listener);
        //监听view渲染完成
        sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);【见小节3.4】
    }
}

3.4 [-> MultiListener.java]

@Override
public void onViewDetachedFromWindow(View v) {
    removeListeners();
    //移除销毁的view
    sPendingTransitions.remove(mSceneRoot);
    ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
    if (runningTransitions != null && runningTransitions.size() > 0) {
        for (Transition runningTransition : runningTransitions) {
            runningTransition.resume(mSceneRoot);
        }
    }
    mTransition.clearValues(true);
}

@Override
public boolean onPreDraw() {
    removeListeners();

    // Don't start the transition if it's no longer pending.
    if (!sPendingTransitions.remove(mSceneRoot)) {//移除它保证只执行一次
        return true;
    }

    // Add to running list, handle end to remove it
    final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
            getRunningTransitions();
    ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
    ArrayList<Transition> previousRunningTransitions = null;
    if (currentTransitions == null) {
        currentTransitions = new ArrayList<Transition>();
        runningTransitions.put(mSceneRoot, currentTransitions);
    } else if (currentTransitions.size() > 0) {
        previousRunningTransitions = new ArrayList<Transition>(currentTransitions);
    }
    //把当前正在运行动画记住
    currentTransitions.add(mTransition);
    mTransition.addListener(new TransitionListenerAdapter() {
        @Override
        public void onTransitionEnd(Transition transition) {
            ArrayList<Transition> currentTransitions =
                    runningTransitions.get(mSceneRoot);
            //执行完移除这个动画
            currentTransitions.remove(transition);
            transition.removeListener(this);
        }
    });
    mTransition.captureValues(mSceneRoot, false);
    //如果有未执行完,上次的遗留动画则恢复它
    if (previousRunningTransitions != null) {
        for (Transition runningTransition : previousRunningTransitions) {
            runningTransition.resume(mSceneRoot);
        }
    }
    mTransition.playTransition(mSceneRoot); //开始运行动画 【见小节3.5】

    return true;
}

3.4 [-> Transition.java]

void playTransition(ViewGroup sceneRoot) {
    mStartValuesList = new ArrayList<TransitionValues>();
    mEndValuesList = new ArrayList<TransitionValues>();
    matchStartAndEnd(mStartValues, mEndValues);
    
    ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
    int numOldAnims = runningAnimators.size();
    WindowId windowId = sceneRoot.getWindowId();
    for (int i = numOldAnims - 1; i >= 0; i--) {
        Animator anim = runningAnimators.keyAt(i);
        if (anim != null) {
            AnimationInfo oldInfo = runningAnimators.get(anim);
            if (oldInfo != null && oldInfo.view != null && oldInfo.windowId == windowId) {
                TransitionValues oldValues = oldInfo.values;
                View oldView = oldInfo.view;
                TransitionValues startValues = getTransitionValues(oldView, true);
                TransitionValues endValues = getMatchedTransitionValues(oldView, true);
                if (startValues == null && endValues == null) {
                    endValues = mEndValues.viewValues.get(oldView);
                }
                boolean cancel = (startValues != null || endValues != null) &&
                        oldInfo.transition.isTransitionRequired(oldValues, endValues);
                if (cancel) {
                    if (anim.isRunning() || anim.isStarted()) {
                        if (DBG) {
                            Log.d(LOG_TAG, "Canceling anim " + anim);
                        }
                        anim.cancel();
                    } else {
                        if (DBG) {
                            Log.d(LOG_TAG, "removing anim from info list: " + anim);
                        }
                        runningAnimators.remove(anim);
                    }
                }
            }
        }
    }
    //把待运行的动画存到mAnimators
    createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);【见小节3.5】
    //运行mAnimators动画
    runAnimators();【见小节3.6】
}

3.5 [-> Transition.java]

protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
        TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
        ArrayList<TransitionValues> endValuesList) {
    if (DBG) {
        Log.d(LOG_TAG, "createAnimators() for " + this);
    }
    ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
    long minStartDelay = Long.MAX_VALUE;
    int minAnimator = mAnimators.size();
    SparseLongArray startDelays = new SparseLongArray();
    int startValuesListCount = startValuesList.size();
    for (int i = 0; i < startValuesListCount; ++i) {
        TransitionValues start = startValuesList.get(i);
        TransitionValues end = endValuesList.get(i);
        if (start != null && !start.targetedTransitions.contains(this)) {
            start = null;
        }
        if (end != null && !end.targetedTransitions.contains(this)) {
            end = null;
        }
        if (start == null && end == null) {
            continue;
        }
        // Only bother trying to animate with values that differ between start/end
        boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
        if (isChanged) {
            if (DBG) {
                View view = (end != null) ? end.view : start.view;
                Log.d(LOG_TAG, "  differing start/end values for view " + view);
                if (start == null || end == null) {
                    Log.d(LOG_TAG, "    " + ((start == null) ?
                            "start null, end non-null" : "start non-null, end null"));
                } else {
                    for (String key : start.values.keySet()) {
                        Object startValue = start.values.get(key);
                        Object endValue = end.values.get(key);
                        if (startValue != endValue && !startValue.equals(endValue)) {
                            Log.d(LOG_TAG, "    " + key + ": start(" + startValue +
                                    "), end(" + endValue + ")");
                        }
                    }
                }
            }
            // createAnimator是由子类去实现的,因为动画是可变。【见小节3.5.1】
            Animator animator = createAnimator(sceneRoot, start, end);
            if (animator != null) {
                // Save animation info for future cancellation purposes
                View view = null;
                TransitionValues infoValues = null;
                if (end != null) {
                    view = end.view;
                    String[] properties = getTransitionProperties();
                    if (properties != null && properties.length > 0) {
                        infoValues = new TransitionValues(view);
                        TransitionValues newValues = endValues.viewValues.get(view);
                        if (newValues != null) {
                            for (int j = 0; j < properties.length; ++j) {
                                infoValues.values.put(properties[j],
                                        newValues.values.get(properties[j]));
                            }
                        }
                        int numExistingAnims = runningAnimators.size();
                        for (int j = 0; j < numExistingAnims; ++j) {
                            Animator anim = runningAnimators.keyAt(j);
                            AnimationInfo info = runningAnimators.get(anim);
                            if (info.values != null && info.view == view &&
                                    ((info.name == null && getName() == null) ||
                                            info.name.equals(getName()))) {
                                if (info.values.equals(infoValues)) {
                                    // Favor the old animator
                                    animator = null;
                                    break;
                                }
                            }
                        }
                    }
                } else {
                    view = (start != null) ? start.view : null;
                }
                if (animator != null) {
                    if (mPropagation != null) {
                        long delay = mPropagation
                                .getStartDelay(sceneRoot, this, start, end);
                        startDelays.put(mAnimators.size(), delay);
                        minStartDelay = Math.min(delay, minStartDelay);
                    }
                    //把animator对象存起来
                    AnimationInfo info = new AnimationInfo(view, getName(), this,
                            sceneRoot.getWindowId(), infoValues);
                    runningAnimators.put(animator, info);
                    mAnimators.add(animator);
                }
            }
        }
    }
    if (startDelays.size() != 0) {
        for (int i = 0; i < startDelays.size(); i++) {
            int index = startDelays.keyAt(i);
            Animator animator = mAnimators.get(index);
            long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay();
            animator.setStartDelay(delay);
        }
    }
}

3.5.1 [-> MaterialContainerTransform.java]

@Nullable
@Override
public Animator createAnimator(
    @NonNull ViewGroup sceneRoot,
    @Nullable TransitionValues startValues,
    @Nullable TransitionValues endValues) {
  if (startValues == null || endValues == null) {
    return null;
  }

  RectF startBounds = (RectF) startValues.values.get(PROP_BOUNDS);
  ShapeAppearanceModel startShapeAppearanceModel =
      (ShapeAppearanceModel) startValues.values.get(PROP_SHAPE_APPEARANCE);
  if (startBounds == null || startShapeAppearanceModel == null) {
    Log.w(TAG, "Skipping due to null start bounds. Ensure start view is laid out and measured.");
    return null;
  }

  RectF endBounds = (RectF) endValues.values.get(PROP_BOUNDS);
  ShapeAppearanceModel endShapeAppearanceModel =
      (ShapeAppearanceModel) endValues.values.get(PROP_SHAPE_APPEARANCE);
  if (endBounds == null || endShapeAppearanceModel == null) {
    Log.w(TAG, "Skipping due to null end bounds. Ensure end view is laid out and measured.");
    return null;
  }

  final View startView = startValues.view;
  final View endView = endValues.view;
  final View drawingView;
  View boundingView;
  View drawingBaseView = endView.getParent() != null ? endView : startView;
  if (drawingViewId == drawingBaseView.getId()) {
    drawingView = (View) drawingBaseView.getParent();
    boundingView = drawingBaseView;
  } else {
    drawingView = findAncestorById(drawingBaseView, drawingViewId);
    boundingView = null;
  }

  // Calculate drawable bounds and offset start/end bounds as needed
  RectF drawingViewBounds = getLocationOnScreen(drawingView);
  float offsetX = -drawingViewBounds.left;
  float offsetY = -drawingViewBounds.top;
  RectF drawableBounds = calculateDrawableBounds(drawingView, boundingView, offsetX, offsetY);
  startBounds.offset(offsetX, offsetY);
  endBounds.offset(offsetX, offsetY);

  boolean entering = isEntering(startBounds, endBounds);

  if (!appliedThemeValues) {
    // Apply theme values if we didn't already apply them up front in the constructor and if they
    // haven't already been set by the user.
    maybeApplyThemeValues(drawingBaseView.getContext(), entering);
  }
  
  //转换主要依靠这个自定义的Drawable来实现的【见小节3.5.2】
  final TransitionDrawable transitionDrawable =
      new TransitionDrawable(
          getPathMotion(),
          startView,
          startBounds,
          startShapeAppearanceModel,
          getElevationOrDefault(startElevation, startView),
          endView,
          endBounds,
          endShapeAppearanceModel,
          getElevationOrDefault(endElevation, endView),
          containerColor,
          startContainerColor,
          endContainerColor,
          scrimColor,
          entering,
          elevationShadowEnabled,
          FadeModeEvaluators.get(fadeMode, entering),
          FitModeEvaluators.get(fitMode, entering, startBounds, endBounds),
          buildThresholdsGroup(entering),
          drawDebugEnabled);

  // Set the bounds of the transition drawable to not exceed the bounds of the drawingView.
  transitionDrawable.setBounds(
      Math.round(drawableBounds.left),
      Math.round(drawableBounds.top),
      Math.round(drawableBounds.right),
      Math.round(drawableBounds.bottom));

  ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
  animator.addUpdateListener(
      new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
          //从0-1的动画百分比同步给drawable去绘制样式
          transitionDrawable.setProgress(animation.getAnimatedFraction());
        }
      });

  addListener(
      new TransitionListenerAdapter() {
        @Override
        public void onTransitionStart(@NonNull Transition transition) {
          // Add the transition drawable to the root ViewOverlay
          // 把转换的背景样式添加到界面上
          ViewUtils.getOverlay(drawingView).add(transitionDrawable);

          // Hide the actual views at the beginning of the transition
          //把真实的布局中开始view和结束view隐藏起来,显示我们转换样式
          startView.setAlpha(0);
          endView.setAlpha(0);
        }

        @Override
        public void onTransitionEnd(@NonNull Transition transition) {
          removeListener(this);
          if (holdAtEndEnabled) {
            // Keep drawable showing and views hidden (useful for Activity return transitions)
            return;
          }
          // Show the actual views at the end of the transition
          //动画结束了把布局中2个转换的view显示出来
          startView.setAlpha(1);
          endView.setAlpha(1);
          
          // 把转换的背景从界面移除
          // Remove the transition drawable from the root ViewOverlay
          ViewUtils.getOverlay(drawingView).remove(transitionDrawable);
        }
      });

  return animator;
}

3.5.2 [-> TransitionDrawable.java]

@Override
public void draw(@NonNull Canvas canvas) {
  if (scrimPaint.getAlpha() > 0) {
    canvas.drawRect(getBounds(), scrimPaint);
  }

  int debugCanvasSave = drawDebugEnabled ? canvas.save() : -1;

  if (elevationShadowEnabled && currentElevation > 0) {
    drawElevationShadow(canvas);
  }

  // Clip the canvas to container's path. Anything drawn to the canvas after this clipping will
  // be masked inside the clipped area.
  maskEvaluator.clip(canvas);

  maybeDrawContainerColor(canvas, containerPaint);
  
  //当MaterialContainerTransform.fadeMode为FADE_MODE_IN时fadeMode.endOnTop是true
  //默认走if先绘制startView后endView 
  if (fadeModeResult.endOnTop) {
    drawStartView(canvas);
    drawEndView(canvas);
  } else {
    drawEndView(canvas);
    drawStartView(canvas);
  }

  if (drawDebugEnabled) {
    canvas.restoreToCount(debugCanvasSave);
    drawDebugCumulativePath(canvas, currentStartBounds, debugPath, Color.MAGENTA);
    drawDebugRect(canvas, currentStartBoundsMasked, Color.YELLOW);
    drawDebugRect(canvas, currentStartBounds, Color.GREEN);
    drawDebugRect(canvas, currentEndBoundsMasked, Color.CYAN);
    drawDebugRect(canvas, currentEndBounds, Color.BLUE);
  }
}

因为TransitionDrawable是自定义Drawable所以主要看draw方法,

3.6 [-> Transition.java]

protected void runAnimators() {
    if (DBG) {
        Log.d(LOG_TAG, "runAnimators() on " + this);
    }
    start();//回调动画的监听 onTransitionStart 方法
    ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
    // Now start every Animator that was previously created for this transition
    for (Animator anim : mAnimators) {
        if (DBG) {
            Log.d(LOG_TAG, "  anim: " + anim);
        }
        if (runningAnimators.containsKey(anim)) {
            start();
            runAnimator(anim, runningAnimators);//去执行动画【见小节3.7】
        }
    }
    mAnimators.clear();
    end();//回调动画的监听 onTransitionEnd 方法
}

3.7 [-> Transition.java]

private void runAnimator(Animator animator,
        final ArrayMap<Animator, AnimationInfo> runningAnimators) {
    if (animator != null) {
        // TODO: could be a single listener instance for all of them since it uses the param
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                mCurrentAnimators.add(animation);
            }
            @Override
            public void onAnimationEnd(Animator animation) {
                runningAnimators.remove(animation);
                mCurrentAnimators.remove(animation);
            }
        });
        animate(animator);//真正执行动画【见小节3.8】
    }
}

3.8 [-> Transition.java]

protected void animate(Animator animator) {
    // TODO: maybe pass auto-end as a boolean parameter?
    if (animator == null) {
        end();
    } else {
        if (getDuration() >= 0) {
            animator.setDuration(getDuration());
        }
        if (getStartDelay() >= 0) {
            animator.setStartDelay(getStartDelay() + animator.getStartDelay());
        }
        if (getInterpolator() != null) {
            animator.setInterpolator(getInterpolator());
        }
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                end();
                animation.removeListener(this);
            }
        });
        animator.start();//开始执行动画
    }
}

四. 小结

可见,View与View之间的动画转换,它本质利用自定义drawable来实现的;通过属性动画0~1的百分比,来不断计算一些属性值,drawable赋值当view上来实现的;

github: github.com/hy-liuyuzhe…