Android 自定义View (一)

序言

自定义View可以实现系统原生View无法满足的需求,并且可以根据开发中的不同需求实现自己的View。通过自定义View,还可以实现一些炫酷的效果,提升产品的用户体验。 一.自定义view的四个构造函数什么场景下触发

1. View(Context)

在java代码中 new的时候调用

2. View(Context mcontext, Attributeset attrs)

在xml声明时调用, attrs是从xml中解析出来的属性集合

3. View(Context mcontext, Attributeset attrs,int defStyleAttr)

仅当我们主动显式调用,例如:在第二个构造函数调用第三个构造函数;第三个参数是手动传入的style值,style配置默认的属性值。

4.View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)

android 21时加上的;如果第三个为0,或者没有定义时,第四个参数才起作用。 如果同时使用这几种方式给 View 设置了属性那么 View 听谁的呢?

xml中直接定义 > xml 中 style 定义 >defStyleAttr > defStyleRes > Theme 中直接定义

自定义View的几种类型:

类型 定义
自定义组合控件 多个组件组合成一个新的控件,方便多次复用
继承系统View控件 如继承TextView,在系统控件的基础功能进行拓展
继承系统的ViewGroup 继承如LinearLayout等系统控件,在其基础上进行拓展
继承View/ViewGroup 不复用系统控件,完全自定义
  1. 自定义组合控件:eg 将筛选功能用自定义组合控件封装成一个组合控件,并适当的将部分逻辑功能放在这里也是合适的。 需要我们在构造函数里将布局加载出来 :
csharp 复制代码
private void initView() {
    View view = View.inflate(getContext(), R.layout.filter_view, this);
}   
  1. 继承系统view:eg:项目中使用较多的是带圆角、各种不同颜色填充颜色、边框颜色的TextView、LinearLayout...。步骤:a.先继承TextView ;b.获取xml自定义属性,c.在构造函数里设置TextView的背景样式,drawable继承GradientDrawable。
  2. 继承View、ViewGroup 不复用系统控件,完全自定义,这类相当复杂。这里我们展开看一下。

自定义View中的处理

实线用原生代码很好实现,画一条虚线就需要我们通过自定义View来实现,具体代码如下:

ini 复制代码
package cn.sto.sxz.core.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import cn.sto.sxz.core.R;

/**
 * author:justin
 * time:2019/08/12
 * desc:
 */
public class MyDashView extends View {
    private static final String TAG = "DashView";
    public static final int DEFAULT_DASH_WIDTH = 100;
    public static final int DEFAULT_LINE_WIDTH = 100;
    public static final int DEFAULT_LINE_HEIGHT = 10;
    public static final int DEFAULT_LINE_COLOR = 0x9E9E9E;

    /**
     * 虚线的方向
     */
    public static final int ORIENTATION_HORIZONTAL = 0;
    public static final int ORIENTATION_VERTICAL = 1;
    /**
     * 默认为水平方向
     */
    public static final int DEFAULT_DASH_ORIENTATION = ORIENTATION_HORIZONTAL;
    /**
     * 间距宽度
     */
    private float dashWidth;
    /**
     * 线段高度
     */
    private float lineHeight;
    /**
     * 线段宽度
     */
    private float lineWidth;
    /**
     * 线段颜色
     */
    private int lineColor;
    private int dashOrientation;

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int widthSize;
    private int heightSize;

    public MyDashView(Context context) {
        this(context, null);
    }

    public MyDashView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DashView);
        dashWidth = typedArray.getDimension(R.styleable.DashView_dashView_dashWidth, DEFAULT_DASH_WIDTH);
        lineHeight = typedArray.getDimension(R.styleable.DashView_dashView_lineHeight, DEFAULT_LINE_HEIGHT);
        lineWidth = typedArray.getDimension(R.styleable.DashView_dashView_lineWidth, DEFAULT_LINE_WIDTH);
        lineColor = typedArray.getColor(R.styleable.DashView_dashView_lineColor, DEFAULT_LINE_COLOR);
        dashOrientation = typedArray.getInteger(R.styleable.DashView_dashView_dashOrientation, DEFAULT_DASH_ORIENTATION);
        mPaint.setColor(lineColor);
        mPaint.setStrokeWidth(lineHeight);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        widthSize = MeasureSpec.getSize(widthMeasureSpec);
        heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int deafaultWidth = 100;
        int defaultHeight = 100;
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        

        Log.d(TAG, "onMeasure: " + widthSize + "----" + heightSize);
        Log.d(TAG, "dashOrientation: " + dashOrientation);
        if (dashOrientation == ORIENTATION_HORIZONTAL) {
            //不管在布局文件中虚线高度设置为多少,虚线的高度统一设置为实体线段的高度
            if (mode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(Math.min(deafaultWidth, widthSize), (int) lineHeight);
            } else {
                setMeasuredDimension(Math.min(heightSize, defaultHeight), (int) lineHeight);
            }
        } else {
            if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST){
                setMeasuredDimension(Math.min(defaultHeight,MeasureSpec.getSize(heightMeasureSpec)), heightSize);
            }else {
                setMeasuredDimension((int) lineHeight, heightSize);
            }
            
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        switch (dashOrientation) {
            case ORIENTATION_VERTICAL:
                drawVerticalLine(canvas);
                break;
            default:
                drawHorizontalLine(canvas);
        }
    }

    /**
     * 画水平方向虚线
     *
     * @param canvas
     */
    public void drawHorizontalLine(Canvas canvas) {

        int paddingLeft = getPaddingLeft();
//        paddingLeft =0;
        int paddingRight = getPaddingRight();
        float totalWidth = paddingLeft;
        canvas.save();
        float[] pts = {paddingLeft, 0, lineWidth + paddingLeft, 0};
        //在画线之前需要先把画布向下平移办个线段高度的位置,目的就是为了防止线段只画出一半的高度
        //因为画线段的起点位置在线段左下角
        canvas.translate(0, lineHeight / 2);
        while (totalWidth <= widthSize - paddingRight) {
            canvas.drawLines(pts, mPaint);
            canvas.translate(lineWidth + dashWidth, 0);
            totalWidth += lineWidth + dashWidth;
        }
        canvas.restore();
    }

    /**
     * 画竖直方向虚线
     *
     * @param canvas
     */
    public void drawVerticalLine(Canvas canvas) {

        int  paddingTop = 0, paddingBottom = 0;
        paddingBottom =getPaddingBottom();
        float totalWidth = paddingTop;
        paddingTop =getPaddingTop();
        canvas.save();
        float[] pts = {0, paddingTop, 0, lineWidth+paddingTop};
        //在画线之前需要先把画布向右平移半个线段高度的位置,目的就是为了防止线段只画出一半的高度
        //因为画线段的起点位置在线段左下角
        canvas.translate(lineHeight / 2, 0);
        while (totalWidth <= heightSize-paddingBottom) {
            canvas.drawLines(pts, mPaint);
            canvas.translate(0, lineWidth + dashWidth);
            totalWidth += lineWidth + dashWidth;
        }
        canvas.restore();
    }
}

总结:首先我们在构造函数里获取自定义View的属性,然后在onMeasure里处理 wrap_content,如若不处理设置的wrap_content 不会生效相当于 match_parent,最后在onDraw()里处理padding属性。

自定义ViewGroup

自定义ViewGroup不仅需要处理 padding,wrap_content也需要处理 margin;我们用具体代码看一下处理的逻辑:

布局文件

js 复制代码
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.bellnet.customview1.CustomViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_red_light"
        android:padding="10dp">

        <com.bellnet.customview1.CustomView
            android:layout_width="250dp"
            android:layout_height="wrap_content"
            app:backgroundColor="@android:color/holo_blue_bright"/>

        <com.bellnet.customview1.CustomView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:backgroundColor="@android:color/holo_green_dark"/>

    </com.bellnet.customview1.CustomViewGroup>


</RelativeLayout>

我们来看一下效果

ini 复制代码
public class CustomViewGroup extends ViewGroup {

    private int paddingLeft;
    private int paddingRight;
    private int paddingTop;
    private int paddingBottom;
    private int viewsWidth;//所有子View的宽度之和(在该例子中仅代表宽度最大的那个子View的宽度)
    private int viewsHeight;//所有子View的高度之和
    private int viewGroupWidth = 0;//ViewGroup算上padding之后的宽度
    private int viewGroupHeight = 0;//ViewGroup算上padding之后的高度
    private int marginLeft
    private int marginTop;
    private int marginRight;
    private int marginBottom;

    public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        viewsWidth = 0;
        viewsHeight = 0;
        marginLeft = 0;
        marginTop = 0;
        marginRight = 0;
        marginBottom = 0;
        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            viewsHeight += childView.getMeasuredHeight();
            viewsWidth = Math.max(viewsWidth, childView.getMeasuredWidth());
            marginLeft = Math.max(0,lp.leftMargin);//在本例中找出最大的左边距
            marginTop += lp.topMargin;//在本例中求出所有的上边距之和
            marginRight = Math.max(0,lp.rightMargin);//在本例中找出最大的右边距
            marginBottom += lp.bottomMargin;//在本例中求出所有的下边距之和
        }
        /* 用于处理ViewGroup的wrap_content情况 */
        viewGroupWidth = paddingLeft + viewsWidth + paddingRight + marginLeft + marginRight;
        viewGroupHeight = paddingTop + viewsHeight + paddingBottom + marginTop + marginBottom;
        setMeasuredDimension(measureWidth(widthMeasureSpec, viewGroupWidth), measureHeight
                (heightMeasureSpec, viewGroupHeight));
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        if (b) {
            int childCount = getChildCount();
            int mTop = paddingTop;
            for (int j = 0; j < childCount; j++) {
                View childView = getChildAt(j);
                MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams();
                int mLeft = paddingLeft + lp.leftMargin;
                mTop += lp.topMargin;
                //  测量所有的子View
                childView.layout(mLeft, mTop, mLeft + childView.getMeasuredWidth(), mTop + childView.getMeasuredHeight());
                mTop += (childView.getMeasuredHeight() + lp.bottomMargin);
            }
        }
    }

    private int measureWidth(int measureSpec, int viewGroupWidth) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                /* 将剩余宽度和所有子View + padding的值进行比较,取小的作为ViewGroup的宽度 */
                result = Math.min(viewGroupWidth, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private int measureHeight(int measureSpec, int viewGroupHeight) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                /* 将剩余高度和所有子View + padding的值进行比较,取小的作为ViewGroup的高度 */
                result = Math.min(viewGroupHeight, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }
}

总结:

  1. 自定义View中处理padding,只需在onDraw()中处理,同时别忘记在在onMeasure()中处理wrap_content;
  2. 自定义ViewGroup处理padding,一般只需要在onLayout()中,给子View布局时算上padding值即可(自定View还需要处理onDraw场景较少)
  3. 自定义View无需处理margin,自定义ViewGroup处理margin时,需要在onMeasure()根据margin计算ViewGroup的宽高,同时在onLayout布局子View别忘记根据margin来布局。
相关推荐
清云随笔4 分钟前
axios 实现 无感刷新方案
前端
鑫宝Code5 分钟前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线13 分钟前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf
pink大呲花16 分钟前
关于番外篇-CSS3新增特性
前端·css·css3
少年维持着烦恼.20 分钟前
第八章习题
前端·css·html
我是哈哈hh23 分钟前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
田本初40 分钟前
如何修改npm包
前端·npm·node.js
明辉光焱1 小时前
[Electron]总结:如何创建Electron+Element Plus的项目
前端·javascript·electron
牧码岛1 小时前
Web前端之汉字排序、sort与localeCompare的介绍、编码顺序与字典顺序的区别
前端·javascript·web·web前端