1.CoordinatorLayout是什么?
CoordinatorLayout is a super-powered FrameLayout.
在谷歌官方文檔中解釋CoordinatorLayout是一個(gè)超級(jí)幀布局,可在兩種情況下使用:
1.作為窗口布局的頂層父布局
2.作為與一個(gè)或者多個(gè)子視圖進(jìn)行交互的容器
通過(guò)給CoordinatorLayout的直接子控件指定一個(gè)Behavior可以在不同的兄弟控件之間得到許多不同的交互,Behaviors可以被用來(lái)實(shí)現(xiàn)各種各樣的交互動(dòng)效,
例如:滑動(dòng)列表時(shí)的懸浮按鈕自動(dòng)顯示與隱藏,滑動(dòng)時(shí)頭部控件的縮放、位移等。
2.CoordinatorLayout是如何協(xié)調(diào)子控件進(jìn)行交互的?
CoordinatorLayout本身不具備實(shí)際交互能力,所有的交互行為都會(huì)被分發(fā)給子view的Behavior去實(shí)現(xiàn),如果子view都沒(méi)有指定Behavior,只能作為一個(gè)FrameLayout存在。
CoordinatorLayout實(shí)現(xiàn)了自己的LayoutParams,而B(niǎo)ehavior就存儲(chǔ)在LayoutParams中,由于子控件的LayoutParams類(lèi)型是由父控件決定的,所以能擁有CoordinatorLayout.LayoutParams的控件只能是CoordinatorLayout的直接子view。
這在一定程度上也使得交互產(chǎn)生了一定的局限性??梢詮浹a(bǔ)這個(gè)局限性的功能是CoordinatorLayout實(shí)現(xiàn)了NestedScrollingParent接口,而嵌套滾動(dòng)的實(shí)現(xiàn)是可以多層級(jí)傳遞事件的。和普通實(shí)現(xiàn)NestedScrollingParent的父控件向上傳遞事件的行為不同的是
CoordinatorLayout會(huì)向下傳遞滾動(dòng)事件,依然是傳遞到Behavior中。
所以一個(gè)實(shí)現(xiàn)了NestedScrollingChild接口的view可以直接包含在CoordinatorLayout的直接子view中,且可以多層級(jí)包含,而CoordinatorLayout的直接子view不需要再次實(shí)現(xiàn)NestedScrollingParent接口。
總結(jié)一句話就是CoordinatorLayout會(huì)攔截作用于它的一切行為并分發(fā)給它的所有直接子view。這使得不同子view之間的行為相互影響成為可能。
3.CoordinatorLayout是如何攔截行為并分發(fā)的?
這里我們可以進(jìn)入源碼先觀察一下CoordinatorLayout是如何測(cè)量并布局子view的,如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren(); //準(zhǔn)備子view
ensurePreDrawListener();
//省略若干代碼...
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//省略若干代碼...
final Behavior b = lp.getBehavior();
//如果子view的Behavior為空,或者子view的測(cè)量方法返回false就使用CoordinatorLayout默認(rèn)的
//onMeasureChild方式進(jìn)行測(cè)量。如果子view的onMeasureChild返回true表示子view自己已經(jīng)進(jìn)行了測(cè)量。
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
}
//省略若干代碼...
setMeasuredDimension(width, height);
}
以上可看見(jiàn)CoordinatorLayout實(shí)際上在自己的各個(gè)行為處理方法中優(yōu)先通知詢(xún)問(wèn)子view是否要先處理,如果子view處理了,則不會(huì)再處理。
但是這只是把處理權(quán)限賦予了子view,在每個(gè)子view之間并沒(méi)有建立相互的的行為關(guān)聯(lián)。但在以上方法中有兩處關(guān)鍵信息不能忽視,就是prepareChildren()和ensurePreDrawListener()兩個(gè)方法。
下面繼續(xù)追蹤源碼:
private void prepareChildren() {
//mDependencySortedChildren用于存儲(chǔ)有依賴(lài)于其他子view行為的子view
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
final View view = getChildAt(i);
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
// Now iterate again over the other children, adding any dependencies to the graph
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// Make sure that the other node is added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(other, view);
}
}
}
// Finally add the sorted graph list to our list
mDependencySortedChildren.addAll(mChildDag.getSortedList());
// We also need to reverse the result since we want the start of the list to contain
// Views which have no dependencies, then dependent views after that
Collections.reverse(mDependencySortedChildren);
}
/**
* Add or remove the pre-draw listener as necessary.
*/
void ensurePreDrawListener() {
boolean hasDependencies = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (hasDependencies(child)) {
hasDependencies = true;
break;
}
}
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
//如果有依賴(lài)則添加相應(yīng)的視圖繪制監(jiān)聽(tīng)回調(diào)
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
/**
* Add the pre-draw listener if we're attached to a window and mark that we currently
* need it when attached.
*/
void addPreDrawListener() {
if (mIsAttachedToWindow) {
// Add the listener
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
//獲取視圖樹(shù)觀察者,添加CoordinatorLayout自身實(shí)現(xiàn)的mOnPreDrawListener監(jiān)聽(tīng)
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
// Record that we need the listener regardless of whether or not we're attached.
// We'll add the real listener when we become attached.
mNeedsPreDrawListener = true;
}
//CoordinatorLayout內(nèi)部類(lèi)OnPreDrawListener,相當(dāng)簡(jiǎn)潔,只是調(diào)用了onChildViewsChanged方法
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
從上面可以追蹤到監(jiān)聽(tīng)被依賴(lài)子view的繪制之后會(huì)調(diào)用onChildViewsChanged方法,下面來(lái)看一下onChildViewsChanged中做了什么
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
//省略代碼若干...
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
//省略代碼若干...
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
//獲取Behavior是否是依賴(lài)于當(dāng)前遍歷的子view,如果是則調(diào)用Behavior相關(guān)方法進(jìn)行回調(diào)通知依賴(lài)view的改變
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
//在通知依賴(lài)view改變之前先檢查是否存在嵌套滾動(dòng),如果存在則此次不通知,因?yàn)榇舜问录?//并沒(méi)有作用于當(dāng)前依賴(lài)view而是通過(guò)嵌套滾動(dòng)機(jī)制傳遞給嵌套view。
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
從以上代碼分析我們知道在測(cè)量子view時(shí)的大致流程如下:

4.一個(gè)實(shí)現(xiàn)自定義Behavior的小例子

以上的例子實(shí)現(xiàn)了紅色控件依賴(lài)于按鈕的縮放屬性,當(dāng)按鈕縮放時(shí)跟著縮放,同時(shí)獲取依賴(lài)控件的文本值計(jì)算結(jié)果。主要展示了依賴(lài)控件可以監(jiān)聽(tīng)到被依賴(lài)控件的重新繪制變化,以及可以獲取被依賴(lài)控件的相關(guān)屬性值等。
以下是例子的具體實(shí)現(xiàn):
package com.example.myapplication;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import java.util.Random;
public class TestBehaviorActivity extends AppCompatActivity {
Button button;
Random random = new Random();
boolean flag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_behavior);
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
button.setText(random.nextInt(10) + " + " + random.nextInt(10));
//改變控件屬性使控件重新繪制,以便CoordinatorLayout通知Behavior的回調(diào)函數(shù)
if (flag) {
button.setScaleX(1);
} else {
button.setScaleX(2);
}
flag = !flag;
}
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.example.yychen.myapplication.TestBehaviorActivity">
<Button
android:id="@+id/button"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:textSize="14sp"
android:layout_marginBottom="100dp"
android:text="1+2"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:padding="10dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:gravity="center"
android:textSize="16sp"
android:textColor="#fff"
android:background="@color/colorAccent"
app:layout_behavior=".TestBehavior"
android:layout_marginTop="100dp"
android:text="依賴(lài)view"/>
</android.support.design.widget.CoordinatorLayout>
package com.example.myapplication;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TestBehavior extends CoordinatorLayout.Behavior<TextView> {
//在布局中使用 app:layout_behavior=".TestBehavior"方式,必須實(shí)現(xiàn)的一個(gè)方法
public TestBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 設(shè)置依賴(lài)于哪個(gè)控件,這里設(shè)置依賴(lài)于類(lèi)型為Button的控件,返回true表示依賴(lài)
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
return dependency instanceof Button;
}
//依賴(lài)的view發(fā)生改變,這里的dependency和layoutDependsOn設(shè)定的依賴(lài)控件一致
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
Button button = (Button) dependency;
String text = button.getText().toString();
child.setScaleX(button.getScaleX());
try {
int[] num = getNum(text);
child.setText("兩數(shù)之和 :" + (num[0] + num[1]));
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
//依賴(lài)的view在窗體中被移除消失
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, TextView child, View dependency) {
super.onDependentViewRemoved(parent, child, dependency);
}
public int[] getNum(String text) throws Exception {
int[] numArray = new int[2];
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher(text);
matcher.find();
String num = matcher.group();
numArray[0] = Integer.valueOf(num);
matcher.find();
num = matcher.group();
numArray[1] = Integer.valueOf(num);
return numArray;
}
}
5.CoordinatorLayout.Behavior相關(guān)信息列舉

6.嵌套滾動(dòng)簡(jiǎn)析
1、什么是嵌套滾動(dòng)? 嵌套滾動(dòng)就是存在兩個(gè)滾動(dòng)行為的相互嵌套,當(dāng)一個(gè)開(kāi)始滾動(dòng)另一個(gè)可以隨著滾動(dòng)的行為。
在一般的交互過(guò)程中 ,嵌套的滾動(dòng)是單一的行為,子view的滾動(dòng)影響這父view,父view并不影響子view。
所以一般的嵌套滾動(dòng)交互流程圖部分如下:

2、關(guān)于嵌套滾動(dòng)相關(guān)核心類(lèi)有以下兩組:
被滾動(dòng)類(lèi)需實(shí)現(xiàn):NestedScrollingParent接口并實(shí)例化NestedScrollingParentHelper幫助類(lèi)
滾動(dòng)類(lèi)需實(shí)現(xiàn):NestedScrollingChild接口并實(shí)例化NestedScrollingChildHelper幫助類(lèi)
NestedScrollingParentHelper、NestedScrollingChildHelper承擔(dān)了實(shí)現(xiàn)相應(yīng)接口的具體嵌套操作。在實(shí)現(xiàn)接口的相關(guān)類(lèi)中調(diào)用幫助類(lèi)的對(duì)應(yīng)方法即可完成嵌套流程。
3、NestedScrollingParent和NestedScrollingChild接口簡(jiǎn)析圖


7.一個(gè)實(shí)現(xiàn)嵌套滾動(dòng)的小例子

上面的例子滾動(dòng)流程分為兩塊,向上滑動(dòng)和向下滑動(dòng)處理邏輯的先后順序正好完全相反。
向上滑動(dòng):先滑動(dòng)父view,再滑動(dòng)子view,再滑動(dòng)子view中的內(nèi)容。
向下滑動(dòng),先滑動(dòng)子view中的內(nèi)容,再滑動(dòng)子view,再滑動(dòng)父view。
下面來(lái)看下具體實(shí)現(xiàn):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.nestedscroll.nesteddemo.NestedScrollParentView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffeebb"
android:layout_alignParentBottom="true"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="Parent" />
<com.nestedscroll.nesteddemo.NestedScrollChildView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#eeaa00"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:padding="20dp"
android:text="Child"
android:textColor="#fff" />
</com.nestedscroll.nesteddemo.NestedScrollChildView>
</com.nestedscroll.nesteddemo.NestedScrollParentView>
</RelativeLayout>
package com.nestedscroll.nesteddemo;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class NestedScrollChildView extends LinearLayout implements NestedScrollingChild {
private NestedScrollingChildHelper helper;
private int[] consumed = new int[2];
private int[] offsetInWindow = new int[2];
private float lastY;
private float initY;
public NestedScrollChildView(Context context) {
super(context);
init();
}
public NestedScrollChildView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedScrollChildView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
helper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
initY = getY();
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
helper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return helper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return helper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
helper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return helper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int
dyUnconsumed, int[] offsetInWindow) {
return helper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return helper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return helper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return helper.dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
lastY = event.getRawY();
//通知父view要開(kāi)始滑動(dòng)了
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:
float deltaY = lastY - event.getRawY();
lastY = event.getRawY();
if (deltaY > 0) { //嵌套子view向上滑動(dòng)時(shí)
//通知父view滑動(dòng)
if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {
// 如果父view消耗了滑動(dòng),需減去消耗距離
deltaY -= consumed[1];
}
//父view消耗的距離為int類(lèi)型,損耗了精度,這里向下取整去掉剩余值
if (Math.floor(deltaY) == 0) {
break;
}
//滑動(dòng)子view
if (getY() - deltaY >= 0) {
setY(getY() - deltaY);
deltaY = 0;
} else {
deltaY -= getY();
setY(0);
}
//滑動(dòng)子view內(nèi)容
if (getScrollY() + deltaY <= (getMeasuredHeight() - 40) / 2) {
scrollBy(0, (int) deltaY);
} else {
scrollTo(0, (getMeasuredHeight() - 40) / 2);
}
} else { // 嵌套子view向下滑動(dòng)時(shí)
float dyConsumed; //子view滑動(dòng)消耗距離
//滑動(dòng)子view內(nèi)容
if (getScrollY() + deltaY >= -(getMeasuredHeight() - 40) / 2) {
scrollBy(0, (int) deltaY);
dyConsumed = deltaY;
} else {
scrollTo(0, -(getMeasuredHeight() - 40) / 2);
dyConsumed = getScrollY() + (getMeasuredHeight() - 40) / 2;
}
float dyUnconsumed = deltaY - dyConsumed;
//滑動(dòng)子view
if (getY() - dyUnconsumed <= initY) {
setY(getY() - dyUnconsumed);
dyConsumed += dyUnconsumed;
} else {
dyConsumed = (int) (dyConsumed + (initY - getY()));
setY(initY);
}
//通知父view滑動(dòng)
dispatchNestedScroll(0, (int) dyConsumed, 0, (int) (deltaY - dyConsumed), offsetInWindow);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//通知父view停止本次嵌套滑動(dòng)
stopNestedScroll();
break;
}
return true;
}
}
package com.nestedscroll.nesteddemo;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
public class NestedScrollParentView extends LinearLayout implements NestedScrollingParent {
NestedScrollingParentHelper helper;
private float initY;
public NestedScrollParentView(Context context) {
super(context);
init();
}
public NestedScrollParentView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedScrollParentView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
helper = new NestedScrollingParentHelper(this);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
initY = getY();
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
helper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
helper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (getY() - dyUnconsumed <= initY) {
setY(getY() - dyUnconsumed);
} else {
setY(initY);
}
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (getY() - dy >= 0) {
setY(getY() - dy);
consumed[1] = dy;
} else {
consumed[1] = (int) getY();
setY(0);
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return helper.getNestedScrollAxes();
}
}
8.在CoordinatorLayout中的嵌套滾動(dòng)
前面分析CoordinatorLayout實(shí)現(xiàn)了NestedScrollingParent接口,而RecyclerView實(shí)現(xiàn)了NestedScrollingChild接口,
利用CoordinatorLayout和RecyclerView可以快速實(shí)現(xiàn)上面例子的嵌套滾動(dòng),以及需要的視圖依賴(lài)變換。