美文网首页Android
Android自定义View

Android自定义View

作者: Timmy_zzh | 来源:发表于2021-02-25 21:37 被阅读0次
    当Android SDK中提供的系统UI控件无法满足业务需求时,就需要考虑自己实现UI控件
    • 自定义UI控件有2种方式
      • 继承系统提供的成熟控件(比如LinearLayout,RelativeLayout,ImageView等)
      • 直接继承自系统View或者ViewGroup,并自绘现实内容

    1.继承现有控件

    • 实现自定义的Toolbar控件CustomeToolbar,CustomeToolbar继承自RelativeLayout,在构造函数中通过addView方法添加两个ImageView和1个TextView。

    效果图:

    1.自定义View-继承自系统控件.png

    代码:

    /**
     * 自定义ToolBar:
     * 左边返回图片,中间文本,右边图片
     * 1。获取自定义属性
     * 2。设置 文本与左右图片的位置
     */
    public class CustomeToolbar extends RelativeLayout {
    
        private String myTitleText;
        private int myTitleTextColor;
        private float myTitleTextSize;
        private Drawable leftImgDrawable;
        private Drawable rightImgDrawable;
    
        public CustomeToolbar(Context context, AttributeSet attrs) {
            super(context, attrs);
            //准备控件
            ImageView leftImg = new ImageView(context);
            ImageView rightImg = new ImageView(context);
            leftImg.setImageDrawable(leftImgDrawable);
            rightImg.setImageDrawable(rightImgDrawable);
            TextView titleTextView = new TextView(context);
            titleTextView.setText(myTitleText);
            titleTextView.setTextColor(myTitleTextColor);
            titleTextView.setTextSize(myTitleTextSize);
    
            //添加控件,并设置排放规则
            this.addView(leftImg, leftImgParams);
            this.addView(rightImg, rightImgParams);
            this.addView(titleTextView, titleParams);
        }
    }
    

    2.自定义属性

    • 当我们想再xml布局文件中使用CustomeToolbar时,希望能在xml文件中直接指定title的显示内容,字体颜色,leftImage和rightImage的显示图片等。
    • 这就需要使用自定义属性,具体步骤分为一下几步
    2.1.attrs.xml文件中声明自定义属性
    • 在res/values目录下attrs.xml文件中(没有就新建一个),使用标签自定义属性,如下:
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="CustomeToolbar">
            <!--居中的文本-->
            <attr name="myTitleText" format="string|reference" />
            <!--文本颜色-->
            <attr name="myTitleTextColor" format="color|reference" />
            <!--文本大小-->
            <attr name="myTitleTextSize" format="dimension|reference" />
            <!--左边图片-->
            <attr name="lefeImgSrc" format="reference" />
            <!--右边图片-->
            <attr name="rightImgSrc" format="reference" />
        </declare-styleable>
    </resources>
    
    • 解析
      • declare-styleable 标签代表定义一个自定义的属性集合,一般会与自定义控件结合使用
      • attr 标签是某一条具体的属性,name是属性名称,format代表属性的格式
    2.2.在xml不仅文件中使用自定义属性
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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:orientation="vertical">
    
        <com.timmy.demopractice.view.CustomeToolbar
            android:layout_width="match_parent"
            android:layout_height="68dp"
            app:lefeImgSrc="@mipmap/ic_back"
            android:background="#ffff00"
            app:myTitleText="自定义View"
            app:myTitleTextColor="#ff0000"
            app:myTitleTextSize="10sp"
            app:rightImgSrc="@mipmap/ic_launcher" />
    
    </LinearLayout>
    
    
    2.3.在CustomeToolbar中,获取自定义属性的引用值
        private void initAttr(Context context, AttributeSet attrs) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomeToolbar);
            myTitleText = ta.getString(R.styleable.CustomeToolbar_myTitleText);
            myTitleTextColor = ta.getColor(R.styleable.CustomeToolbar_myTitleTextColor, Color.BLACK);
            //已由sp转换为px
            myTitleTextSize = ta.getDimension(R.styleable.CustomeToolbar_myTitleTextSize, 14);
            leftImgDrawable = ta.getDrawable(R.styleable.CustomeToolbar_lefeImgSrc);
            rightImgDrawable = ta.getDrawable(R.styleable.CustomeToolbar_rightImgSrc);
            ta.recycle();
        }
    
    • 主要通过Context.obtainStyledAttributes方法获取到自定义属性的集合,然后从这个集合中取出相应的自定义属性
    • 继承自系统控件的自定义View,除了通过调用addView方法添加子控件,还可以通过 LayoutInflater.from方法填充xml布局文件,如下:
        public CustomeToolbar(Context context, AttributeSet attrs) {
            super(context, attrs);
            LayoutInflater.from(context).inflate(R.layout.xxx, this);
        }
    

    3.直接继承自View或者ViewGroup

    • 这种自定义View实现麻烦一些,但是更加灵活,也能实现更加复杂的UI界面,实现过程中需要解决以下几个问题
      • 如何根据相应的属性将UI元素绘制到界面
      • 自定义控件的大小,也就是宽和高分别设置多少
      • 如果是ViewGroup,如何合理安排其内部子View的排放位置
    • 以上3个问题可以在如下3个方法中得到解决:
      • onDraw
      • onMeasure
      • onLayout
    • 所以自定义View的重点工作其实就是复写并合理实现这3个方法。
    3.1.onDraw
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
        }
    
    • onDraw方法接收一个Canvas类型的参数。Canvas可以理解为一个画布,在这块画布上可以绘制各种类型的UI
    • 系统提供了一系列Canvas操作方法如下:

    Canvas

    public class Canvas extends BaseCanvas {
      void drawArc(RectF oval, startAngle, float sweepAngle, useCenter,Paint paint) 绘制弧形
        void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) 绘制图片
        void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) 绘制圆形
        void drawLine(float startX, float startY, float stopX, float stopY,Paint paint) 绘制直线
        void drawOval(@NonNull RectF oval, @NonNull Paint paint) 绘制椭圆
        void drawPath(@NonNull Path path, @NonNull Paint paint) 绘制path路径
        void drawPoint(float x, float y, @NonNull Paint paint) 绘制点
        void drawRect(Rect r, @NonNull Paint paint) 绘制矩形区域
        void drawRoundRect(RectF rect, float rx, float ry, Paint paint) 绘制圆角矩形
        void drawText(String text, float x, float y, @NonNull Paint paint) 绘制文本
    }
    
    调用Canvas类的draw方法最终会调用BaseCanvas中的native方法
    

    Paint

    • 在Canvas的各种draw方法中,都需要传入一个Paint对象。Paint相当于一个画笔,通过设置画笔的各种属性,来实现不同绘制效果:
    public class Paint {
        void setStyle(Style style) 设置绘制模式
        void setColor(@ColorInt int color) 设置画笔颜色
        void setAlpha(int a) 设置画笔透明度
        void setStrokeWidth(float width) 设置线条宽度
        void setStrokeCap(Cap cap) 设置画笔绘制两端时的样式
        void setStrokeJoin(Join join) 设置画笔绘制时,折线的样式
        Shader setShader(Shader shader) 设置Paint的填充效果
        ColorFilter setColorFilter(ColorFilter filter) 设置画笔线的样式
        public Xfermode setXfermode(Xfermode xfermode) 设置画笔的层叠效果
        public Typeface setTypeface(Typeface typeface) 设置字体样式
        void setTextSize(float textSize) 设置文本字体大小
        void setAntiAlias(boolean aa) 设置抗锯齿开关
        void setDither(boolean dither) 设置防抖动开关
    }
    
    实现圆环进行条控件
    1. 自定义控件
    /**
     * 绘制扇形进度控件:绘制一个圆,和其中代表进度的扇形
     * 1。接收自定义属性- 原的颜色,扇形的颜色等
     * 2。初始化Paint(2)
     * 3。onDraw方法中绘制圆形和扇形
     * 在onSizeChange方法中获取到绘制的区域
     */
    public class PieImageView extends View {
    
        private Paint arcPaint;
        private Paint circlePaint;
        private RectF mBound;
        private int radius;
    
        public PieImageView(Context context) {
            this(context, null);
        }
    
        public PieImageView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public PieImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initPaint(context);
        }
    
        private void initPaint(Context context) {
            arcPaint = new Paint();
            arcPaint.setAntiAlias(true);
            arcPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            arcPaint.setStrokeWidth(dpTopx(0.1f, context));
            arcPaint.setColor(Color.BLUE);
    
            circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            circlePaint.setStyle(Paint.Style.STROKE);
            circlePaint.setStrokeWidth(dpTopx(2, context));
            circlePaint.setColor(Color.RED);
    
            mBound = new RectF();
        }
    
        /**
         * 拿到控件的宽高
         * 设置圆形绘制的半径
         * 设置圆形绘制的区域 mBound
         */
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            Log.e("Tim", "onSizeChanged w:" + w + " ,h:" + h);
            int min = Math.min(w, h);
            radius = min / 3;
            mBound.set(min / 2 - radius, min / 2 - radius, min / 2 + radius, min / 2 + radius);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            Log.e("Tim", "onDraw");
            canvas.drawCircle(mBound.centerX(), mBound.centerY(), radius, circlePaint);
            canvas.drawArc(mBound, 0, 125, true, arcPaint);
        }
    
        float density = 0;
    
        /**
         * dp转px
         */
        private int dpTopx(float dp, Context context) {
            if (density == 0) {//密度
                density = context.getResources().getDisplayMetrics().density;
            }
            Log.e("Tim", "density:" + density);
            return (int) (dp * density);
        }
    }
    
    
    1. 在xml中使用
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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:orientation="vertical">
        
        <com.timmy.demopractice.view.PieImageView
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="@color/colorAccent" />
    
    </LinearLayout>
    
    1. 最后效果图
    2.自定义View固定宽高.png
    当自定义PieImageView的宽高设置为wrap_content的时候,控件展示效果如下:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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:orientation="vertical">
    
        <com.timmy.demopractice.view.PieImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorAccent" />
    
    </LinearLayout>
    
    3.自定义View-包裹宽高.png
    • 问题解析
      • 当自定义控件PieImageView设置的宽高为wrap_content(自适应)时,PieImageView并没有正常显示,问题的主要原因是并没有在onMeasure方法中进行重新测量,并重新设置宽高
    3.2.onMeasure
    自定义View为什么需要重新测量
    • 正常情况下,直接在xml布局文件中定义好View的宽高,然后让自定义View在此区域内显示即可。

    • 但是为了更好地兼容不同尺寸的屏幕,Android系统提供了wrap_content和match_parent属性来规范控件的现实规则

      • wrap_content代表自适应大小,match_parent代表填充父视图大小
      • 但是这两个属性并没有指定具体的大小,因此我们需要在onMeasure方法中过滤出这两种情况,真正的测量出自定义View应该现实的宽高大小。
    • onMeasure系统方法如下:

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
    • onMeasure方法会传入2个参数widthMeasureSpec和heightMeasureSpec。
      • 这两个参数是从父视图传递给子View的两个参数
      • widthMeasureSpec,heightMeasureSpec值不仅仅表示的是宽和高,还有一个非常重要的测量模式,这个值由宽高的具体值和测量模式组合而成
    MeasureSpec
    • 如果通过onMeasure的参数获取到当前View的宽高值和测量模式呢?
        //宽度测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        //宽度测量大小
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        //高度测量模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //高度测量大小
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
    • 测量模式一共分为3种
      • EXACTLY:表示在xml布局文件中宽高使用match_parent或者固定大小的宽高
      • AT_MOST:表示在xml布局文件中宽高使用wrap_content
      • UNSPECIFIED:父容器没有对当前View有任何限制,当前View可以取人意尺寸,比如ListView中的item
    • widthMeasureSpec 和 heightMeasureSpec的组成内容
      • 上面两个值都是int类型,他是如何表示测量模式和测量大小的呢?
      • int类型共有32位:其中二进制高2位表示测量模式,低30位表示宽高具体大小
    自定义PieImageView处理
    • 因为上述代码PieImageView中我们没有复写onMeasure方法,因此会默认调用父类View的onMeasure方法,其默认实现如下:
    public class View implements ... {
      
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), 
                                                widthMeasureSpec),
                                 getDefaultSize(getSuggestedMinimumHeight(),
                                                        heightMeasureSpec)
                                );
        }
    
        public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
      
        protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight :
          max(mMinHeight,   mBackground.getMinimumHeight());
        }
    }
    
    • 解析
      • onMeasure方法最终会调用setMeasuredDimension方法,该方法传入的值直接决定View的宽高
      • getDefaultSize 方法返回的是默认大小,默认为父视图的剩余可用空间
      • 所以我们在xml文件中设置PieImageView的宽高为wrap_content,实际使用的是父视图的剩余可用空间

    代码修改,处理onMeasure方法:

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //宽度测量模式
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            //宽度测量大小
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            //高度测量模式
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            //高度测量大小
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode) {
                int size = Math.min(widthSize, heightSize);
                setMeasuredDimension(size, size);
            } else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            }
        }
    

    最终效果:

    4.自定义View-复写onMeasure方法.png

    相关文章

      网友评论

        本文标题:Android自定义View

        本文链接:https://www.haomeiwen.com/subject/ntqpfltx.html