官方文檔的描述ViewTreeObserver是用來監(jiān)聽一些全局變化的样漆。
在 ViewTreeObserver 中怒见,包含了以下幾個接口:
interface ViewTreeObserver.OnGlobalFocusChangeListener
interface ViewTreeObserver.OnGlobalLayoutListener
interface ViewTreeObserver.OnPreDrawListener
interface ViewTreeObserver.OnScrollChangedListener
interface ViewTreeObserver.OnTouchModeChangeListener
ViewTreeObserver 注冊一個觀察者來監(jiān)聽視圖樹盯拱,當視圖樹的布局事甜、視圖樹的焦點订歪、視圖樹將要繪制穿肄、視圖樹滾動等發(fā)生改變時年局,ViewTreeObserver都會收到通知,ViewTreeObserver不能被實例化咸产,可以調(diào)用View.getViewTreeObserver()來獲得矢否。
ViewTreeObserver繼承關系:
public final class ViewTreeObserverextendsObject
java.lang.Object
android.view.ViewTreeObserver
ViewTreeObserver直接繼承自Object.
ViewTreeObserver提供了View的多種監(jiān)聽,每一種監(jiān)聽都有一個內(nèi)部類接口與之對應脑溢,內(nèi)部類接口全部保存在CopyOnWriteArrayList中僵朗,通過ViewTreeObserver.addXXXListener()來添加這些監(jiān)聽,源碼如下:
public final class ViewTreeObserver {
// Recursive listeners use CopyOnWriteArrayList
private CopyOnWriteArrayList<OnWindowFocusChangeListener> mOnWindowFocusListeners;
private CopyOnWriteArrayList<OnWindowAttachListener> mOnWindowAttachListeners;
private CopyOnWriteArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
private CopyOnWriteArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
private CopyOnWriteArrayList<OnEnterAnimationCompleteListener> mOnEnterAnimationCompleteListeners;
// Non-recursive listeners use CopyOnWriteArray
// Any listener invoked from ViewRootImpl.performTraversals() should not be recursive
private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners;
private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;
private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners;
// These listeners cannot be mutated during dispatch
private ArrayList<OnDrawListener> mOnDrawListeners;
}
以OnGlobalLayoutListener為例屑彻,首先是定義接口:
public interface OnGlobalLayoutListener {
/**
* Callback method to be invoked when the global layout state or the visibility of views
* within the view tree changes
*/
public void onGlobalLayout();
}
將OnGlobalLayoutListener 添加到CopyOnWriteArray數(shù)組中:
/**
* Register a callback to be invoked when the global layout state or the visibility of views
* within the view tree changes
*
* @param listener The callback to add
*
* @throws IllegalStateException If {@link #isAlive()} returns false
*/
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
}
mOnGlobalLayoutListeners.add(listener);
}
移除OnGlobalLayoutListener验庙,當視圖樹布局發(fā)生變化時不會再收到通知了:
/**
* Remove a previously installed global layout callback
*
* @param victim The callback to remove
*
* @throws IllegalStateException If {@link #isAlive()} returns false
*
* @deprecated Use #removeOnGlobalLayoutListener instead
*
* @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
*/
@Deprecated
public void removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) {
removeOnGlobalLayoutListener(victim);
}
/**
* Remove a previously installed global layout callback
*
* @param victim The callback to remove
*
* @throws IllegalStateException If {@link #isAlive()} returns false
*
* @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
*/
public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
return;
}
mOnGlobalLayoutListeners.remove(victim);
}
其他常用方法:
dispatchOnGlobalLayout():視圖樹發(fā)生改變時通知觀察者,如果想在View Layout 或 View hierarchy 還未依附到Window時社牲,或者在View處于GONE狀態(tài)時強制布局粪薛,這個方法也可以手動調(diào)用。
/**
* Notifies registered listeners that a global layout happened. This can be called
* manually if you are forcing a layout on a View or a hierarchy of Views that are
* not attached to a Window or in the GONE state.
*/
public final void dispatchOnGlobalLayout() {
// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
// perform the dispatching. The iterator is a safe guard against listeners that
// could mutate the list by calling the various add/remove methods. This prevents
// the array from being modified while we iterate it.
final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
access.get(i).onGlobalLayout();
}
} finally {
listeners.end();
}
}
}
dispatchOnPreDraw():通知觀察者繪制即將開始搏恤,如果其中的某個觀察者返回 true违寿,那么繪制將會取消让禀,并且重新安排繪制,如果想在View Layout 或 View hierarchy 還未依附到Window時陨界,或者在View處于GONE狀態(tài)時強制繪制,可以手動調(diào)用這個方法痛阻。
/**
* Notifies registered listeners that the drawing pass is about to start. If a
* listener returns true, then the drawing pass is canceled and rescheduled. This can
* be called manually if you are forcing the drawing on a View or a hierarchy of Views
* that are not attached to a Window or in the GONE state.
*
* @return True if the current draw should be canceled and resceduled, false otherwise.
*/
@SuppressWarnings("unchecked")
public final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
cancelDraw |= !(access.get(i).onPreDraw());
}
} finally {
listeners.end();
}
}
return cancelDraw;
}
ViewTreeObserver常用內(nèi)部類:
內(nèi)部類接口 | 備注 |
---|---|
ViewTreeObserver.OnPreDrawListener | 當視圖樹將要被繪制時菌瘪,會調(diào)用的接口 |
ViewTreeObserver.OnGlobalLayoutListener | 當視圖樹的布局發(fā)生改變或者View在視圖樹的可見狀態(tài)發(fā)生改變時會調(diào)用的接口 |
ViewTreeObserver.OnGlobalFocusChangeListener | 當一個視圖樹的焦點狀態(tài)改變時,會調(diào)用的接口 |
ViewTreeObserver.OnScrollChangedListener | 當視圖樹的一些組件發(fā)生滾動時會調(diào)用的接口 |
ViewTreeObserver.OnTouchModeChangeListener | 當視圖樹的觸摸模式發(fā)生改變時阱当,會調(diào)用的接口 |
獲得View高度的幾種方式:
我們應該都遇到過在onCreate()方法里面調(diào)用view.getWidth()和view.getHeight()獲取到的view的寬高都是0的情況俏扩,這是因為在onCreate()里還沒有執(zhí)行測量,需要在onResume()之后才能得到正確的高度弊添,那么可不可以在onCreate()里就得到寬高呢录淡?答:可以!常用的有下面幾種方式:
1油坝、通過設置View的MeasureSpec.UNSPECIFIED來測量:
int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(w, h);
//獲得寬高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();
設置我們的SpecMode為UNSPECIFIED嫉戚,然后去調(diào)用onMeasure測量寬高,就可以得到寬高澈圈。
2彬檀、通過ViewTreeObserver .addOnGlobalLayoutListener來獲得寬高,當獲得正確的寬高后瞬女,請移除這個觀察者窍帝,否則回調(diào)會多次執(zhí)行:
//獲得ViewTreeObserver
ViewTreeObserver observer=view.getViewTreeObserver();
//注冊觀察者,監(jiān)聽變化
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//判斷ViewTreeObserver 是否alive诽偷,如果存活的話移除這個觀察者
if(observer.isAlive()){
observer.removeGlobalOnLayoutListener(this);
//獲得寬高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();
}
}
});
3坤学、通過ViewTreeObserver .addOnPreDrawListener來獲得寬高,在執(zhí)行onDraw之前已經(jīng)執(zhí)行了onLayout()和onMeasure()报慕,可以得到寬高了深浮,當獲得正確的寬高后,請移除這個觀察者卖子,否則回調(diào)會多次執(zhí)行
//獲得ViewTreeObserver
ViewTreeObserver observer=view.getViewTreeObserver();
//注冊觀察者略号,監(jiān)聽變化
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if(observer.isAlive()){
observer.removeOnDrawListener(this);
}
//獲得寬高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();
return true;
}
});
案例分析
xml布局如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/layout"
tools:context="trs.com.viewtreeobserverdemo.MainActivity">
<TextView
android:id="@+id/tv_show"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:hint="et1"
android:tag="et1"
android:id="@+id/et_1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:tag="et2"
android:hint="et2"
android:layout_marginTop="10dp"
android:id="@+id/et_2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:text="test"
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
讓MainActivity實現(xiàn)相應接口
public class MainActivity extends AppCompatActivity implements
ViewTreeObserver.OnGlobalLayoutListener,
ViewTreeObserver.OnPreDrawListener, ViewTreeObserver.OnGlobalFocusChangeListener,
ViewTreeObserver.OnTouchModeChangeListener, View.OnClickListener
在onCreat中添加監(jiān)聽
EditText et_1,et_2;
TextView tv_show;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewTreeObserver vto = findViewById(R.id.layout).getViewTreeObserver();
et_1= (EditText) findViewById(R.id.et_1);
et_2= (EditText) findViewById(R.id.et_2);
vto.addOnGlobalLayoutListener(this);
vto.addOnPreDrawListener(this);
vto.addOnGlobalFocusChangeListener(this);
vto.addOnTouchModeChangeListener(this);
findViewById(R.id.btn).setOnClickListener(this);
tv_show= (TextView) findViewById(R.id.tv_show);
}
一.OnGlobalFocusChangeListener
首先測試ViewTreeObserver.OnGlobalFocusChangeListener,實現(xiàn)接口方法
代碼
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
if(oldFocus!=null) {
tv_show.setText("Focus change from " + oldFocus.getTag() + " to " + newFocus.getTag());
}else{
tv_show.setText( newFocus.getTag()+"get focus");
}
}
注意:在第一次進入頁面的時候沒有oldFoucs
效果
這個接口很簡單就是監(jiān)聽focus的變化:
二.OnPreDrawListener
OnPreDrawListener接口是在繪制界面前調(diào)用
代碼
@Override
public boolean onPreDraw() {
et_1.setHint("set hint on onPreDraw ");
//Return true to proceed with the current drawing pass, or false to cancel.
//返回 true 繼續(xù)繪制,返回false取消洋闽。
return true;
}
效果
如果返回false的話玄柠,效果是這樣的橱鹏,界面沒有繪制搭幻。
關于OnPreDrawListener的使用,有一個例子就是CoordinatorLayout調(diào)用Behavior的onDependentViewChanged就是通過注冊OnPreDrawListener接口序苏,在繪制的時候檢查界面是否發(fā)生變化刊懈,如果變化就調(diào)用Behavior的onDependentViewChanged这弧。
源碼
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
//注冊OnPreDrawListener
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
//分發(fā)OnDependentViewChanged
dispatchOnDependentViewChanged(false);
return true;
}
}
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
//判斷是否發(fā)生變化
// Did it change? if not continue
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
// 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();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
//調(diào)用onDependentViewChanged
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
...
}
}
}
}
三.OnGlobalLayoutListener
Interface definition for a callback to be invoked when the global layout state
or the visibility of views within the view tree changes.
當在一個視圖樹中全局布局發(fā)生改變或者視圖樹中的某個視圖的可視狀態(tài)發(fā)生改變時娃闲,所要調(diào)用的回調(diào)函數(shù)的接口類
代碼
1.在點擊時改變EditText的可視性。
@Override
public void onClick(View v) {
if(et_1.isShown()){
et_1.setVisibility(View.GONE);
}else{
et_1.setVisibility(View.VISIBLE);
}
}
2.在onGlobalLayout顯示EditText的可見性
@Override
public void onGlobalLayout() {
if(et_1.isShown()){
tv_show.setText("EditText1 顯示");
}else{
tv_show.setText("EditText1 隱藏");
}
}
效果
注意:在測試的時候發(fā)現(xiàn)使用
et_1.setVisibility(View.INVISIBLE);
時并不會觸發(fā)OnGlobalLayoutListener而只能使用
et_1.setVisibility(View.GONE);
補充
可以使用OnGlobalLayoutListener獲取控件寬高匾浪。
private int mHeaderViewHeight;
private View mHeaderView;
.....
mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mHeaderViewHeight = mHeaderView.getHeight();
mHeaderView.getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
});