前言
先簡單復習一下Android View 的 繪制順序:
1、onMeasure(測量),先根據構造器傳進來的LayoutParams(布局參數),測量view寬高。
2、onLayout(布局),再根據測量出來的寬高參數,進行布局。
3、onDraw(繪制),最后繪制出View。
ps:案例中用到了dataBinding
1、使用LayoutParams改變View高度
效果:getHeight 和 getMeasuredHeight 的值是一樣的,沒有區別;
getWidth 和 getMeasuredWidth 也是同理。
bind.btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) bind.textBox.getLayoutParams(); params.height += 10;bind.textBox.setLayoutParams(params);Log.d("TAG", "更新后 getHeight:" + bind.textBox.getHeight() + " --- getMeasuredHeight:" + bind.textBox.getMeasuredHeight());}});
2、使用layout改變View高度
效果:getHeight 的值在變化,而 getMeasuredHeight 的值沒有變化,還是初始值。
getWidth 和 getMeasuredWidth 也是同理。
bind.btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {bind.textBox.layout(bind.textBox.getLeft(),bind.textBox.getTop(),bind.textBox.getRight(),bind.textBox.getBottom() + 10);Log.d("TAG","更新后 getHeight:" + bind.textBox.getHeight() + " --- getMeasuredHeight:" + bind.textBox.getMeasuredHeight());}});
3、區別
通過以上方式可以看出,使用layout可以改變View 寬或高 度,但并不會更新View原始測量值,即使使用requestLayout()也不行。
源碼:因為它是final類型,不可重寫。
4、兩種獲取寬高值的應用場景
1、View布局完成(就是View執行完onLayout),使用 getWidth / getHeight,這是View源碼。
/*** Return the width of your view.** @return The width of your view, in pixels.*/@ViewDebug.ExportedProperty(category = "layout")public final int getWidth() {return mRight - mLeft;}/*** Return the height of your view.** @return The height of your view, in pixels.*/@ViewDebug.ExportedProperty(category = "layout")public final int getHeight() {return mBottom - mTop;}/*** Right position of this view relative to its parent.** @return The right edge of this view, in pixels.*/@ViewDebug.CapturedViewPropertypublic final int getRight() {return mRight;}/*** Left position of this view relative to its parent.** @return The left edge of this view, in pixels.*/@ViewDebug.CapturedViewPropertypublic final int getLeft() {return mLeft;}/*** Bottom position of this view relative to its parent.** @return The bottom of this view, in pixels.*/@ViewDebug.CapturedViewPropertypublic final int getBottom() {return mBottom;}/*** Top position of this view relative to its parent.** @return The top of this view, in pixels.*/@ViewDebug.CapturedViewPropertypublic final int getTop() {return mTop;}
1.1、自定義View 運行結果
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);Log.d("TAG", "onMeasure --- getLeft:" + getLeft()); // 0Log.d("TAG", "onMeasure --- getTop:" + getTop()); // 0Log.d("TAG", "onMeasure --- getRight:" + getRight()); // 0Log.d("TAG", "onMeasure --- getBottom:" + getBottom()); // 0Log.d("TAG", "onMeasure --- getWidth:" + getWidth()); // 0Log.d("TAG", "onMeasure --- getHeight:" + getHeight()); // 0}@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);Log.d("TAG", "onMeasure --- getLeft:" + getLeft()); // 503Log.d("TAG", "onMeasure --- getTop:" + getTop()); // 871Log.d("TAG", "onMeasure --- getRight:" + getRight()); // 577Log.d("TAG", "onMeasure --- getBottom:" + getBottom()); // 923Log.d("TAG", "onMeasure --- getWidth:" + getWidth()); // 74 = 577 - 503Log.d("TAG", "onMeasure --- getHeight:" + getHeight()); // 52 = 923 - 871}
2、反之View沒有布局完,使用 getMeasuredWidth / getMeasuredHeight,比如在onCreate中使用可以獲取 寬高值,如果在此 使用 getWidth / getHeight 返回會是0,因為此時還沒有布局完成。
注意:在布局未完成前使用 getMeasuredWidth / getMeasuredHeight,要先主動通知系統測量,才會有值,如果布局已經完成,那就直接用,不需要這一步;
通知系統測量方法:measure(0,0),直接都寫0就好,系統會返回正確的值。
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);bind = AppActivityMainBinding.bind(getLayoutInflater().inflate(R.layout.app_activity_main, null));setContentView(bind.getRoot());// 主動通知系統去測量bind.textBox的高度// 注意:在onCreate中,此時的布局還未完成,只有執行了這句代碼,bind.textBox.getMeasuredHeight() 才會有值bind.textBox.measure(0,0);bind.showHeight.setText("getHeight:" + bind.textBox.getHeight() + " --- getMeasuredHeight:" + bind.textBox.getMeasuredHeight()); }
5、使用layout改變View寬高,會引發的問題
前言提到,onLayout(布局)是根據測量出來的寬高參數,進行布局的,雖然在視覺上改變了寬高,但測量的寬高值還是原始值,沒有改變;
而在Android布局體系中,父View負責刷新、布局顯示子View,當子View需要刷新時,則是通知父View來完成,就是循環遍歷調用子View的?measure / layout / draw 方法。
由此得出,當布局中某個子View布局發生改變,這個父View就開始循環遍歷調用子View的layout,通過布爾值changed判斷當前子View是否需要重新布局,changed為true表示當前View的大小或位置改變了 。
這時就會發現 之前通過layout改變寬高的View,會被還原。因為onLayout(布局)是根據測量出來的寬高參數,進行布局的,重要的話說三遍。
案例:
我新加了一個TextView,將值顯示在屏幕上。
<TextViewandroid:id="@+id/show_height"android:layout_width="wrap_content"android:layout_height="wrap_content"/>
View沒有發生改變?不,它改變了,但給TextView賦值后,TextView的寬高發生改變,通知父View刷新,父View開始循環遍歷子View的layout方法,導致 通過layout改變寬高的View,又根據 原始測量值,重新布局還原了,由于執行的太快,所以視覺上看不到View這個過程。
日志:
20:29:39.933 ?D ?MTextView --- onLayout --- bottom:1127 --- changed:true
20:29:39.935 ?D ?更新TextView,觸發MTextView的 onLayout方法
20:29:39.973 ?D ?MTextView --- onLayout --- bottom:1117 --- changed:true
6、案例文件
MTextView.java
public class MTextView extends androidx.appcompat.widget.AppCompatTextView {public MTextView(@NonNull Context context) {super(context);}public MTextView(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int heightPxValue = MeasureSpec.getSize(heightMeasureSpec);Log.d("TAG", "MTextView --- onMeasure --- heightPxValue:" + heightPxValue);}@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);Log.d("TAG", "MTextView --- onLayout --- bottom:" + bottom + " --- changed:" + changed);}
app_activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"><data></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"tools:context=".ui.activity.AppMainActivity"><TextViewandroid:id="@+id/show_height"android:layout_width="wrap_content"android:layout_height="wrap_content"/><Buttonandroid:id="@+id/btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="update"android:textAllCaps="false" /><com.example.xxx.ui.view.MTextViewandroid:id="@+id/text_box"style="@style/Font_303133_15_bold"android:layout_width="200dp"android:layout_height="100dp"android:background="@color/color_66000000"android:gravity="center"android:text="hello world" /></LinearLayout></layout>
AppMainActivity.java
public class AppMainActivity extends AppCompatActivity {private AppActivityMainBinding bind;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);bind = AppActivityMainBinding.bind(getLayoutInflater().inflate(R.layout.app_activity_main, null));setContentView(bind.getRoot());// bind.btn.setOnClickListener(new View.OnClickListener() {// @Override// public void onClick(View v) {// LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) bind.textBox.getLayoutParams(); // params.height += 10;// bind.textBox.setLayoutParams(params);// Log.d("TAG", "更新后 getHeight:" + bind.textBox.getHeight() + " --- getMeasuredHeight:" + bind.textBox.getMeasuredHeight());// }// });// bind.btn.setOnClickListener(new View.OnClickListener() {// @Override// public void onClick(View v) {// bind.textBox.layout(// bind.textBox.getLeft(),// bind.textBox.getTop(),// bind.textBox.getRight(),// bind.textBox.getBottom() + 10// );// Log.d("TAG","更新后 getHeight:" + bind.textBox.getHeight() + " --- getMeasuredHeight:" + bind.textBox.getMeasuredHeight());// }// });bind.btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {bind.textBox.layout(bind.textBox.getLeft(),bind.textBox.getTop(),bind.textBox.getRight(),bind.textBox.getBottom() + 10);bind.showHeight.setText("getHeight:" + bind.textBox.getHeight() + " --- getMeasuredHeight:" + bind.textBox.getMeasuredHeight());Log.d("TAG","更新TextView,觸發MTextView的 onLayout方法");}});}}
總結
在View沒有布局完成前,想要獲取 寬高,使用 getMeasuredWidth / getMeasuredHeight,記得先通知系統測量;
反之只要顯示在屏幕上,getWidth / getHeight 就能拿到值,還是時時數據。
補充一下,如果在xml中給View設置了visibility="gone",注意是xml,getWidth / getHeight 也拿不到值,如果是?visibility="invisible",不受影響。
再如果?在xml中給View設置了visibility="gone",在代碼中設置成setVisibility(View.VISIBLE),第一次也拿不到值,因為還沒有layout完成,之后就可以拿到了,后面再給它設置成setVisibility(View.GONE),也不會受影響,因為已經布局過了。代碼在這,我都試過了,核心就是看View有沒有onLayout完成。
Activity
@Overridepublic void onWindowFocusChanged(boolean hasFocus) {super.onWindowFocusChanged(hasFocus);Log.d("TAG", "getHeight:" + bind.btn.getHeight()); // 0new Handler().postDelayed(new Runnable() {@Overridepublic void run() {Log.d("TAG", "getHeight:" + bind.btn.getHeight()); // 0bind.btn.setVisibility(View.VISIBLE); // 設置顯示Log.d("TAG", "getHeight:" + bind.btn.getHeight()); // 0}}, 2000);new Handler().postDelayed(new Runnable() {@Overridepublic void run() {Log.d("TAG", "getHeight:" + bind.btn.getHeight()); // 126bind.btn.setVisibility(View.GONE); // 設置消失Log.d("TAG", "getHeight:" + bind.btn.getHeight()); // 126}}, 5000);new Handler().postDelayed(new Runnable() {@Overridepublic void run() {Log.d("TAG", "getHeight:" + bind.btn.getHeight()); // 126}}, 8000);}
Xml
<Buttonandroid:id="@+id/btn"android:visibility="gone"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="update"android:textAllCaps="false" />