安卓高级开发之自定义View(一)

    科技2022-08-07  113

    1.什么是自定义View

    安卓中自定义View就是根据需求自己设计一个组件,这就包括继承View或View的派生类,然后去重写内部方法。

    通常来说自定义View分为三种:

    继承View,自定义组件;继承View的派生类,即系统提供的组件;组合型控件,自定义组件中包含了其他组件。

    2.在自定义View的步骤

    1.测量 onMeasure(),即确定控件的长和宽;

    2.布局 onLayout(),即确定控件的摆放位置;

    3.绘画onDraw(),即确定控件的样式。

    在自定义ViewGroup中,一般只要重写onMeasure()和onLayout(),而在自定义View中一般只要重写onMeasure()和onDraw() 。

    3.自定义View的派生类中的ViewGroup,即自定义布局

    本文要实现的是一个流式布局,就像这样:

    具体代码展示:

    public class FlowLayout extends ViewGroup { //保存每行子View集合 private List<List<View>> list; //保存每行的行高 private List<Integer> lineHeights; //view之间的间距 int spacing = 30; public FlowLayout(Context context) { super(context); } public FlowLayout(Context context, AttributeSet attrs) { super(context, attrs); } public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } //初始化集合 private void init(){ if(list != null){ list.clear(); lineHeights.clear(); }else { list = new ArrayList<>(); lineHeights = new ArrayList<>(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //一定要在该方法里面初始化,因为该方法可能运行不止一次 init(); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); //一行有多少子view,记录一下,为layout做准备 List<View> lineViews = new ArrayList<>(); //一行中已经记录了的宽度 int lineWidth = 0; //一行中的最高高度 int lineHeight = 0; //他父类给的宽度 int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); //通过子view测量出的宽度 int width = 0; int height = 0; for (int i = 0;i < getChildCount();i++){ View childView = getChildAt(i); //获取子View属性 //LayoutParams childLP = childView.getLayoutParams(); //获取子View的外边距,直接无法获取到,要重写generateLayoutParams() MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams(); //测量宽度 // int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft + paddingRight,childLP.width); // int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop + paddingBottom,childLP.height); // //设置子View的测量结果 // childView.measure(childWidthMeasureSpec,childHeightMeasureSpec); //设置子View的测试结果 measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0); //获取子View宽度 int childWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); //是否需要换行 if (lineWidth + childWidth + spacing + lp.leftMargin + lp.rightMargin > parentWidth){ //保存每行子View集合 list.add(lineViews); //保存行高 lineHeights.add(lineHeight); //得到最大宽度 width = Math.max(width,lineWidth); //计算高度 height = height + lineHeight + spacing; //换行要进行初始化 lineViews = new ArrayList<>(); lineHeight = 0; lineWidth = 0; } //保存子view lineViews.add(childView); lineWidth = lineWidth + childWidth + spacing + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight,childHeight + lp.topMargin + lp.bottomMargin); //计算最后一行的宽高,防止遗漏 if (i == getChildCount() - 1){ //保存每行子View集合 list.add(lineViews); lineHeights.add(lineHeight); width = Math.max(width,lineWidth); height = height + lineHeight + spacing; } } //获取测量模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //判断是用通过子view计算出的宽高还是父类给予的宽高 int realWidth = (widthMode == MeasureSpec.EXACTLY) ? parentWidth : width + paddingLeft + paddingRight; int realHeight = (heightMode == MeasureSpec.EXACTLY) ? parentHeight : height + paddingTop + paddingBottom; setMeasuredDimension(realWidth,realHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int line = lineHeights.size(); //给子View进行布局 for(int i = 0; i < line; i++){ for(int j = 0; j < list.get(i).size(); j++){ View childView = list.get(i).get(j); MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams(); //计算出位置信息 int left = paddingLeft + lp.leftMargin; int top = paddingTop + lp.topMargin; int right = left + childView.getMeasuredWidth(); int bottom = top + childView.getMeasuredHeight(); //然后进行布局 childView.layout(left,top,right,bottom); paddingLeft = right + spacing; } //换行后要重新计算left和top paddingLeft = getPaddingLeft(); paddingTop = paddingTop + lineHeights.get(i) + spacing; } } //重写该方法 @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(),attrs); } }

    自定义布局代码注释写的很清楚,不在过多解释了。

    但是其中比较重要的就是测量子View的宽高,在计算出布局的宽高。比如这个方法:

    //设置子View的测试结果 measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0);

    我们可以看一下这个方法:

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //测量子View的宽高 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); //设置子View的测量结果 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }

    继续查看这个方法:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // 当父类的宽或者高,是确定值时如:match_parent,100dp case MeasureSpec.EXACTLY: //子View宽或者高是自己设置的值,如100dp if (childDimension >= 0) { //那么子View的宽或者高就是自己设定的值和精确模式 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子view宽度或者高度是最大值时,那么子view就是最大值和精确模式 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View的宽或者高是自适应时,宽或者高最大不能超过父类给的最大值, //模式也是不确定的,即自适应模式。 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 当父类的宽或者高是不确定,如WRAP_CONTENT时 case MeasureSpec.AT_MOST: if (childDimension >= 0) { // 子view是自己设置的确定值时,宽或者高就是自己设置的值,模式就是精确的 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子view是最大值,宽或者高就是最大值,模式就是不确定的 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View的宽或者高是自适应时,宽或者高最大不能超过父类给的最大值, //模式也是不确定的,即自适应模式。 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 当父类不设限模式,如ListView,他的内容可以超过屏幕限制,一般用不到 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 子view是自己设置的确定值时,宽或者高就是自己设置的值,模式就是精确的 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 父类对子View大小不设限制 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //保存测量模式和长度 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }

    这里涉及MeasureSpec,这个很重要。

    4.对MeasureSpec的理解

    1.MeasureSpec = SpecMode + SpecSize,即测量模式和测量长度构成。

    测量模式分为三种:

    MeasureSpec.EXACTLY ,精确模式,当控件的layout_width和layout_height属性指定为具体数值或match_parent时。MeasureSpec.AT_MOST,不确定模式,当控件的layout_width和layout_height属性指定为wrap_content时。MeasureSpec.UNSPECIFIED,不设限模式,父类不对子类大小进行限制。

    2.LayoutParams 组件的属性,子 View 自身的测量状态

    如:layout_width和layout_height属性,MeasureSpec可以认为是父View对子View大小的限制,而LayoutParams是子View自身对父View尺寸大小的请求。通过父控件的MeasureSpec和子View的LayoutParams可以确定子View的MeasureSpec,即测量的大小。

    就如上面的方法,getChildMeasureSpec(int spec, int padding, int childDimension)

    要确定子View的宽或者高的测量结果就需要父View的MeasureSpec和子View的LayoutParams。

    5.应用自定义布局

    <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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" android:background="@color/black" tools:context=".MainActivity"> <TextView android:id="@+id/text_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="20dp" android:text="搜索历史" android:textColor="@color/white" android:textStyle="bold" android:textSize="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <com.example.customview.myviewgroup.FlowLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/text_view"> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="苹果手机" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="苹果笔记本" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="苹果无线蓝牙耳机" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="苹果手表" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="苹果TV" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="华为P40" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="华为MateBook" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="小米10" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="小米10至尊纪念版" /> <TextView android:textColor="@color/white" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_shape" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="小米游戏本" /> </com.example.customview.myviewgroup.FlowLayout> </androidx.constraintlayout.widget.ConstraintLayout>

    这样就实现了上面演示的流式布局效果。

    Processed: 0.011, SQL: 8