千里之行,始於足下。如果不豁出性命,將無法創造未來。
想要自定義控件 需要對源碼進行分析,看Android 源碼是如何寫的,可以慢慢進行模仿 手寫 測試,最後熟練掌握成爲自己的一個新技能。
嘗試寫一個常用控件 流式佈局,如下圖
簡單分析: 創建一個類FlowLayout 繼承ViewGrop。需要有幾個構造函數,但是需要實現這幾個構造函數。
我們自定義的佈局,主要是重寫他的onMeasure()和onLayout()方法。
onMeasure 中 MeasureSpec 特別難理解我文字描述一下:
MeasureSpec 本身是一個32位的int值,高兩位代碼的是SpecMode ,低30位代表的是SpecSize,
而SpecMode有三種模式,分別是EXactily、AT_MOST、UNSPECIFIED。通過SpecMode 和SpecSize
計算基本就可以得出view的大小。
比如要計算一個view的大小,用到兩個屬性一個是父View的SpecMode和本身的LayoutParam
(具體指:layout_width和layout_height)。
主要是getChildMeasureSpec()這個方法來計算子View大小。
當父ViewSpecMode 爲 EXACTLY的時候,看子View的LayoutParams 的值
比如:childDimension>=0 , 子View的size == childDimension和mode ==EXACTLY
比如:childDimension==-1 , 子View的size : 要多大 給多大 和mode ==EXACTLY
比如:childDimension==-2 , 子View的size :看父View能給多少 和mode ==AT_MOST ,有最大值。
當父ViewSpecMode 爲 AT_MOST的時候
比如:childDimension>=0 , 子View的size == childDimension和mode ==EXACTLY
比如:childDimension==-1 , 子View的size : 看父View能給多少 和mode ==AT_MOST,有最大值
比如:childDimension==-2 , 子View的size :看父View能給多少 和mode ==AT_MOST ,有最大值。
當父ViewSpecMode 爲 UNSPECIFIED的時候
比如:childDimension>=0 , 子View的size == childDimension和mode ==EXACTLY
比如:childDimension==-1 , 子View的size : 要多大給多大 和 mode ==UNSPECIFIED,不限制但有可能看不見
比如:childDimension==-2 , 子View的size :要多大給多大 和 mode ==UNSPECIFIED ,不限制但有可能看不見。
具體註釋都寫在代碼裏了,注意看
package yuhua.zyh.cn.com.myapplication; import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; /** * 作者:62551 on 2020/3/30 10:44 * 郵箱:[email protected] * 描述:流式佈局 */ public class FlowlayoutYh extends ViewGroup { private int mHorizontalSpacing = dp2px(16); //每個item橫向間距 private int mVerticalSpacing = dp2px(8); //每個item橫向間距 private List<List<View>> allLineViews; private List<Integer> allHeights; public FlowlayoutYh(Context context) { super(context); } // 通過這個構造函數,我們可以使用反射獲取xml中的屬性。 public FlowlayoutYh(Context context, AttributeSet attrs) { super(context, attrs); } //這個構造函數可以設置它的 主題style public FlowlayoutYh(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } //初始化幾個數組 private void initMeasureData(){ allLineViews = new ArrayList<>(); allHeights = new ArrayList<>(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { initMeasureData(); //獲取其子View的個數 int childCount = getChildCount(); // 創建一個ListView用來存儲每一行的子View List<View> lineViews = new ArrayList<>(); //得到FLowLayout 自身的width 和 height int selfWidth = MeasureSpec.getSize(widthMeasureSpec); int selfHeight = MeasureSpec.getSize(heightMeasureSpec); //定義兩個局部變量 用來記錄一行已使用的width 和height int lineUseWidth =0; int lineUseHeight = 0; //定義兩個局部變量 空間本身已使用的width 和height int parentUseWidth = 0; int parentUseHeight = 0; // 通過遍歷 去測量每個子View的大小 for (int i = 0; i <childCount; i++) { //獲取子View 並進行測量,通過查看源碼得知,我們測量一個View大小是, // 是需要通過父View的SpecMode和layoutparams來進行測量的。 // SpecMode 可以通過 onMeasure()的入參 widthMeasureSpec、heightMeasureSpec的高兩位得知, // LayoutParamsk 直接view.getLayoutParams()就可以知道 View childView = getChildAt(i); LayoutParams lp = childView.getLayoutParams(); //通過getChildMeasureSpec(),得子View的Spec 然後進行測量 int childViewWidthSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeft()+getPaddingRight(),lp.width); int childViewHeightSpec = getChildMeasureSpec(heightMeasureSpec,getPaddingTop()+getPaddingBottom(),lp.height); childView.measure(childViewWidthSpec,childViewHeightSpec); //測量後將其加到容器中 lineViews.add(childView); //測量之後纔可以顯示之前,可以通過getMeasuredWidth,getMeasuredHeight 得知 子View的真實大小。 int childViewMeasureWidth =childView.getMeasuredWidth(); int childViewMeasuredHeight = childView.getMeasuredHeight(); //有了子View的大小了,計算得出 已使用的 width 和Height lineUseHeight = Math.max(lineUseHeight,childViewMeasuredHeight);//取一行中最高的作爲行高 // 每一個空間及間距相加就是已使用的行寬 lineUseWidth = childViewMeasureWidth+mHorizontalSpacing+lineUseWidth; //換行 當滿一行時換行 並記錄 if (lineUseWidth+childViewMeasureWidth+mHorizontalSpacing>selfWidth){ //把每一行的views存起來 在onlayout中使用 allLineViews.add(lineViews); //把每一行的行高也存起來 在onlayout中使用 allHeights.add(lineUseHeight); //計算 父View的已用的寬高 parentUseWidth = Math.max(lineUseWidth+mHorizontalSpacing,parentUseWidth); parentUseHeight = parentUseHeight+lineUseHeight+mVerticalSpacing; //已滿一行,這幾個狀態值清零 lineViews =new ArrayList<>(); lineUseHeight =0; lineUseWidth =0; } //還有最後行要單獨判斷,防止不滿一行時不顯示 if (i == childCount-1){ allHeights.add(lineUseHeight); allLineViews.add(lineViews); parentUseWidth = Math.max(lineUseWidth+mHorizontalSpacing,parentUseWidth); parentUseHeight = parentUseHeight+lineUseHeight+mVerticalSpacing; } } int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int measureWidth = widthMode== MeasureSpec.EXACTLY? selfWidth:parentUseWidth ; int measureHeight = heightMode ==MeasureSpec.EXACTLY? selfHeight: parentUseHeight; setMeasuredDimension(measureWidth,measureHeight); } //通過代碼可以添加View public void setViews(List<View> views){ for (int i = 0; i < views.size(); i++) { addView(views.get(i)); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 重新佈局 // 將每一個view進行擺放即可,已經測量好, 所有的View的Left top right bottom //計算好了直接進行layout即可 int childCount = allLineViews.size(); int curL = getPaddingLeft(); int curT= getPaddingTop(); for (int i = 0; i < childCount; i++) { List<View> views = allLineViews.get(i); int height = allHeights.get(i); for (int j = 0; j < views.size(); j++) { View view = views.get(j); int left = curL; int top = curT; int right = left + view.getMeasuredWidth(); int bottom = top+ view.getMeasuredHeight(); view.layout(left,top,right,bottom); //每一個view佈局完後,curL 會變化,值爲 當前view的right + 自定義的間距 curL = right +mHorizontalSpacing; } //每一行結束後 重置left 和top curL = getPaddingLeft(); curT = height+curT+mVerticalSpacing; } } public static int dp2px(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics()); } }
xml使用
<ScrollView 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:text="搜索歷史" android:textColor="@android:color/black" android:textSize="18sp"/> <yuhua.zyh.cn.com.myapplication.FlowlayoutYh android:id="@+id/flowlayout" android:layout_gravity="center_horizontal" android:padding="4dp" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout> </ScrollView>
可以直接在Flowlayout中添加View ,也可以在代碼中添加。
package yuhua.zyh.cn.com.myapplication; import android.os.Bundle; import android.view.View; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import java.util.ArrayList; import java.util.List; /** * 作者:62551 on 2020/3/30 15:20 * 郵箱:[email protected] * 描述:測試自定義控件FlowLayout */ public class TestActivity extends AppCompatActivity implements View.OnClickListener { private List<View> views; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); views = new ArrayList<>(); FlowlayoutYh flowLayoutYuhua= findViewById(R.id.flowlayout); for (int i = 0; i < 12; i++) { TextView textView = new TextView(this); textView.setBackgroundResource(R.drawable.shape_button_circular); textView.setText("第"+(i+1)+"個玩具"); textView.setTag("第"+(i+1)+"個玩具"); textView.setOnClickListener(this); views.add(textView); } flowLayoutYuhua.setViews(views); } @Override public void onClick(View v) { Toast.makeText(this,v.getTag().toString(),Toast.LENGTH_LONG).show(); } }
到此測試完成,自定義控件ok,可以使用,有點擊事件
說明:其中還有好多細節未完成,其中沒有添加 margin屬性。也沒有做 適配器模式,期待後續有時間繼續完善它。