Android ViewTreeObserver使用总结
ViewTreeObserver这是一个注册监听视图树的观察者(observer),当视图树的布局,视图树的焦点,视图树将要绘制,视图树滚动等发生改变时,ViewTreeObserver都会收到通知,都会有回调,ViewTreeObserver不能被实例化,可以通过getViewTreeObserver()来获得。
ViewTreeObserver提供了查看的多种监听,每一种监听都有一个内部类接口与之对应,内部类接口全部保存在的CopyOnWriteArrayList中,通过ViewTreeObserver.addXXXListener()来添加这些监听,源码如下:
// 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;
以OnGlobalLayoutListener为例,首先是定义接口:
/**
* Interface definition for a callback to be invoked when the global layout state
* or the visibility of views within the view tree changes.
*/
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数组中:
/**
* 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,当视图树布局发生变化时不会再收到通知了:
/**
* 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():视图树发生改变时通知观察者,如果想在查看布局或视图层次结构还未依附到Window时,或者在查看处于GONE状态时强制布局,这个方法也可以手动调用。
/**
* 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,那么绘制将会取消,并且重新安排绘制,如果想在视图布局或视图层次结构还未依附到Window时,或者在视图处于GONE状态时强制绘制,可以手动调用这个方法。
/**
* 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常用内部类:
获得视图高度的几种方式:
我们应该都遇到过在的onCreate()方法里面调用view.getWidth()和view.getHeight(),获取到的值都是0的情况,这是因为在的onCreate()里还没有执行测量,需要在的onResume()之后才能得到正确的高度,那么可不可以在的onCreate()里就得到宽高?
等onCreate方法执行完了,我们定义的控件才会被度量(measure),所以我们在onCreate方法里面通过view.getHeight()获取控件的高度或者宽度肯定是0,因为它自己还没有被度量,也就是说他自己都不知道自己有多高,而你这时候去获取它的尺寸,肯定是不行的。
要获取边距view.getLeft(),view.getRight(),获取到的值也都是0的情况,因为要执行完OnLayout(确定位置)后才能获取到边距。
OnMeasure->OnLayout(确定位置)->OnDraw(activity的onCreate方法执行结束之后才会走此流程)
方式1:通过ViewTreeObserver .addOnGlobalLayoutListener来获得宽高,当获得正确的宽高后,请移除这个观察者,否则回调会多次执行: 建议使用该方法
<View
android:id="@+id/view"
android:layout_marginLeft="20dp"
android:paddingLeft="10dp"
android:layout_width="200dp"
android:layout_height="100dp"></View>
mView = findViewById(R.id.view);
Log.e("xyh", "width: " + mView.getWidth()); //0
Log.e("xyh", "height: " + mView.getHeight()); //0
Log.e("xyh", "left: " + mView.getLeft()); //0
Log.e("xyh", "PaddingLeft: " + mView.getPaddingLeft()); //20
// 监听0nLayout方法结束的事件,位置确定好之后再获取圆点间距
mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 移除监听,避免重复回调
//mView.getViewTreeObserver().removeGlobalOnLayoutListener(this); //过时方法
mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
Log.e("xyh", "width1: " + mView.getWidth()); //400
Log.e("xyh", "height1: " + mView.getHeight()); //200
Log.e("xyh", "left1: " + mView.getLeft()); //10
Log.e("xyh", "PaddingLeft1: " + mView.getPaddingLeft()); //20
}
});
方式2:也可以通过设置视图的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为不定的,然后去调用onMeasure测量宽高,就可以得到宽高。
该方法多调用了一次onMeasure()方法,该方法虽然看上去简单,但是如果要目标控件计算耗时比较大的话(如listView等),不建议使用。
方式3:通过ViewTreeObserver .addOnPreDrawListener来获得宽高,在执行onDraw之前已经执行了onLayout()和onMeasure(),可以得到宽高了,当获得正确的宽高后,请移除这个观察者,否则回调会多次执行
//获得ViewTreeObserver
ViewTreeObserver observer=view.getViewTreeObserver();
//注册观察者,监听变化
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;
}
});
实际应用
比如:在项目中有这样一个需求,我的某一个控件需要根据其他控件的高度来确定自己的高度,以达到适配的效果。因此,我需要在整个视图完成布局之后,就获得这些高度的参数,ViewTreeObserver可以完成这样的功能任务。
案例分析
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:layout_height="match_parent"
android:id="@+id/linear_layout"
android:orientation="vertical"
tools:context="com.xiaoyehai.viewtreeobserver.MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/et1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/et2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
public class MainActivity extends AppCompatActivity implements
ViewTreeObserver.OnGlobalLayoutListener,
ViewTreeObserver.OnGlobalFocusChangeListener,
ViewTreeObserver.OnTouchModeChangeListener,
View.OnClickListener,
ViewTreeObserver.OnPreDrawListener {
private TextView mTv;
private EditText mEditText1, mEditText2;
private Button mButton;
private LinearLayout mLinearLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLinearLayout = (LinearLayout) findViewById(R.id.linear_layout);
mTv = (TextView) findViewById(R.id.tv);
mEditText1 = (EditText) findViewById(R.id.et1);
mEditText2 = (EditText) findViewById(R.id.et2);
mButton = (Button) findViewById(R.id.btn);
ViewTreeObserver viewTreeObserver = mLinearLayout.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(this);
viewTreeObserver.addOnPreDrawListener(this);
viewTreeObserver.addOnGlobalFocusChangeListener(this);
viewTreeObserver.addOnTouchModeChangeListener(this);
mButton.setOnClickListener(this);
}
}
OnGlobalLayoutListener
当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态(隐藏显示状态)发生改变时,所要调用的回调函数的接口类。
在点击按钮时改变EditText1上的可视性:
@Override
public void onClick(View v) {
if (mEditText1.getVisibility() == View.VISIBLE) {
mEditText1.setVisibility(View.GONE);
} else {
mEditText1.setVisibility(View.VISIBLE);
}
}
在onGlobalLayout回调中设置的EditText的可见性
/**
* 当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态(隐藏显示状态)发生改变时,所要调用的回调函数的接口类
*/
@Override
public void onGlobalLayout() {
if (mEditText1.getVisibility() == View.VISIBLE) {
mTv.setText("mEditText1 显示");
} else {
mTv.setText("mEditText1 隐藏");
}
}
OnPreDrawListener
OnPreDrawListener接口是在绘制界面前调用
/**
* OnPreDrawListener接口是在绘制界面前调用
*/
@Override
public boolean onPreDraw() {
mEditText1.setHint("s绘制界面前调用 ");
//返回 true 继续绘制,返回false取消。
return true;
}
OnGlobalFocusChangeListener
当视图树中的焦点状态发生更改时要调用的回调的接口定义。
/**
* 当视图树中的焦点状态发生更改时要调用的回调的接口定义。
*
* @param oldFocus
* @param newFocus
*/
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
if (oldFocus != null) {
mTv.setText(oldFocus.getTag() + "to" + newFocus.getTag());
} else {
mTv.setText(newFocus.getTag() + "");
}
}
注意:在第一次进入页面的时候没有oldFoucs。
View.INVISIBLE并不会触发OnGlobalLayoutListener。