本章內容: 瞭解View動畫的總體設計理念,關鍵是思想, 而非代碼細節.
一. 如何讓View動起來.
1. 首先要了解View是如何展示到屏幕上的?
①. 先確定View的位置, 如下圖:
②. 在View上面繪製內容, 如下圖:
2. 得出兩種讓View運動的方案:
①. layout() 改變佈局位置
②. draw() 改變 繪製內容的位置
二. 系統採用的時哪種方案呢?
答:第2種, draw() 繪製時,改變繪製內容的位置.
這樣做的好處:無論如何運動, 保留了原始位置 (相對父控件的位置), 如下圖:
三. View中,setScrollX、setScrollY如何實現滾動的?
從設計者的角度看: 爲了降低耦合度, 我們應該把需要滾動的信息單獨記錄下來,然後在draw()繪製的時候 ,加上需要滾動的座標, 最終的出新的繪製座標, 如下圖:
從源碼實現的角度看:
以setScrollY爲例:
- 給mScrollY 變量賦值
protected int mScrollY;
public void setScrollY(int value) {
scrollTo(mScrollX, value);
}
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
mScrollX = x;
mScrollY = y;
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
- draw()繪製, 關鍵代碼(僞代碼)
public void draw(Canvas canvas) {
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
canvas.saveUnclippedLayer(left、top、right、位置信息);
canvas.drawRect(left、top、right、bootm 位置信息)
}
四. 補間動畫 和屬性動畫 如何實現的?
從設計者的角度看: 原理和上面類似, 先把動畫信息先用單獨對象保存起來,然後在draw()繪製的時候,進行矩陣變化, 如下圖:
從源碼的角度看:
①. 補間動畫保存,其實就是保存到一個變量中 mCurrentAnimation
View.java
protected Animation mCurrentAnimation = null;
public void startAnimation(Animation animation) {
...
setAnimation(animation);
invalidate(true);
}
public void setAnimation(Animation animation) {
mCurrentAnimation = animation;
}
②. 屬性動畫保存, 以setTranslationX爲例,其實就是把值保存到一個變量中mRenderNode
View.java
final RenderNode mRenderNode;
public View(Context context) {
mRenderNode = RenderNode.create(getClass().getName(), new ViewAnimationHostBridge(this));
}
public void setTranslationX(float translationX) {
if (translationX != getTranslationX()) {
mRenderNode.setTranslationX(translationX);
invalidateViewProperty(false, true);
}
③. 繪製過程
View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
Transformation transformToApply = null;
//獲取補間動畫
final Animation a = getAnimation();
if (a != null) {
//根據時間, 計算出需要的矩陣變化信息,並用Transformation包起來
applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
//這時transformToApply裏面已經有變換矩陣的信息
transformToApply = parent.getChildTransformation();
}
//canvas 矩陣變換
if (transformToApply != null) {
canvas.concat(transformToApply.getMatrix());
}
//獲取屬性動畫的矩陣變化信息,canvas 矩陣變換
if (!childHasIdentityMatrix && !drawingWithRenderNode) {
canvas.concat(getMatrix());
}
//開始常見的draw繪製
draw(canvas);
}
④. 補間動畫,如何計算Matrix()信息 (矩陣變換信息)
Matrix()信息, 實際上是包裹起來的,結構如下圖:
View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
}
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) {
Transformation t = parent.getChildTransformation();
a.getTransformation(drawingTime, t, 1f);
}
Animation.java
public boolean getTransformation(long currentTime, Transformation outTransformation,float scale) {
return getTransformation(currentTime, outTransformation);
}
public boolean getTransformation(long currentTime, Transformation outTransformation) {
//估值器處理, 不是本章重點
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
}
//獲取Matrix()信息,
protected void applyTransformation(float interpolatedTime, Transformation t) {
}
可以看到, Animation類中具體的算法applyTransformation()是空實現, 需要交給子類來實現, 如果需要自定義動畫算法, 關鍵是重寫這個方法, 例如系統提供的平移動畫:TranslateAnimation.java
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float dx = mFromXDelta;
float dy = mFromYDelta;
if (mFromXDelta != mToXDelta) {
dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
}
if (mFromYDelta != mToYDelta) {
dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
}
t.getMatrix().setTranslate(dx, dy);
}
⑤. 屬性動畫,如何計算Matrix()信息 (矩陣變換信息)
mRenderNode就是前面保存的屬性動畫信息,將matrix傳到RenderNode進行賦值, 最後跑到native裏面處理了,具體賦值的算法這裏就不深究了.
View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
canvas.concat(getMatrix());
}
public Matrix getMatrix() {
final Matrix matrix = mTransformationInfo.mMatrix;
mRenderNode.getMatrix(matrix); // 傳進去後, 在裏面給matrix賦值
return matrix;
}
RenderNode.java
public void getMatrix(@NonNull Matrix outMatrix) {
nGetTransformMatrix(mNativeRenderNode, outMatrix.native_instance);
}
@CriticalNative
private static native void nGetTransformMatrix(long renderNode, long nativeMatrix);
五. 動畫是如何做到一幀一幀運動的?
- 每次draw()的時候,都是根據當前時間, 來獲取動畫對應的座標.
- 如果動畫沒到結束時間,調用invalidate(),等待下一次垂直同步信號, 會繼續執行draw(), 詳細的可以去了解 屏幕渲染機制.
六. 補間動畫 和 屬性動畫 對於點擊觸摸事件有什麼不同?
動畫只是位置上有所變化, 所以我們只需要 事件 對於位置是如何判定的就可以了,源碼如下:
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
for (int i = childrenCount - 1; i >= 0; i--) {
View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//判斷觸摸的座標 是否在child內
if (!isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
}
}
protected boolean isTransformedTouchPointInView(float x, float y, View child,PointF outLocalPoint) {
//關鍵:原始座標+屬性動畫 ---> 新的座標
transformPointToViewLocal(point, child); //執行完這一句之後,point就已經算上動畫後的座標了
final boolean isInView = child.pointInView(point[0], point[1]);
return isInView;
}
public void transformPointToViewLocal(float[] point, View child) {
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point); //重新計算point座標
}
}
View.java
//獲取逆矩陣
public final Matrix getInverseMatrix() {
final Matrix matrix = mTransformationInfo.mInverseMatrix;
mRenderNode.getInverseMatrix(matrix);
return matrix;
}
/*package*/ final boolean pointInView(float localX, float localY) {
return pointInView(localX, localY, 0);
}
//最終計算位置
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
結論: 事件處理計算座標的時候,只是把屬性動畫考慮進去了, 並沒有把補間動畫算進去, 所以屬性動畫運動後,點擊觸摸事件是可以觸發的,補間動畫則不行.
思考: 如果補間動畫也需要處理點擊觸摸事件, 那怎麼辦呢?能看懂本章內容的話, 那應該很好解決了.