千里之行,始于足下。如果不豁出性命,将无法创造未来。
想要自定义控件 需要对源码进行分析,看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 * 邮箱:625510236@qq.com * 描述:流式布局 */ 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 * 邮箱:625510236@qq.com * 描述:测试自定义控件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属性。也没有做 适配器模式,期待后续有时间继续完善它。