Android自定义控件

自定义控件

一、自定义控件常见实现方式

1. 继承View 重写onDraw方法

采用这种方式需要自己支持wrap_content,padding也需要自己处理

2. 继承特定的View,如TextView

这种方法主要是用于扩展某种已有的View,增加一些特定的功能。这种方法比较简单,也不需要自己支持wrap_content和padding。

3. 继承ViewGroup,派生特殊的Layout

主要用于实现自定义的布局,看起来很像几种View组合在一起的时候,可以使用这种方式。这种方式需要合适地处理ViewGroup的测量和布局,并同时处理子元素的测量和布局过程。比如自定义一个自动换行的LinerLayout等。

4. 继承特定的ViewGroup,比如LinerLayout

扩展已有的ViewGroup,组合View实现一些特定功能。

二、注意事项(示例可见下方自定义时钟demo)

支持自定义属性
  1. values目录下创建attrs.xml文件
    Attrs中定义的name和自定义控件类名保持一致

  2. 构造方法中获取自定义的属性值


     TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAnalogClock, defStyleAttr,0);
     mColor = a.getColor(R.styleable.CustomAnalogClock_textColor, Color.BLACK);
    
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
         saveAttributeDataForStyleable(context, R.styleable.CustomAnalogClock, attrs, a, defStyleAttr, 0);
     }
     a.recycle();
    
支持wrap_content

重写onMeasure 根据实际业务需要设置默认的宽高 支持wrap_content

支持padding

在计算或绘制是考虑上下左右的padding值

做好生命周期的处理

View 的生命周期的处理思想和 Activity 的处理思想相似,做好 View 中使用的资源处理,如:动画等,总结下来的方式如下:

  1. 做好资源的使用及释放工作。这里主要涉及 onAttachedToWindow() / onDetachedFromWindow() 两个函数的处理,这里需要注意 onDeatachedFromWindow() 的调用仅仅表示该 View 从 Window 上移除,后续是可以重新添加的。
  2. 处理 View 可见性变化处理。这里就看 onVisbilityChanged() 函数。
  3. 处理数据保存于重建工作onSaveInstanceState() 和onRestoreInstanceState() ,该方法和 Activity 中的方法进行对应。
做好内存管理

频繁调用的方法内部尽量避免创建对象,
如:在View的onDraw方法中不要创建太多的临时对象,也就是new出来的对象。因为onDraw方法会被频繁调用,大量的临时对象创建,会引起内存抖动,影响View的效果

package com.zcl.practice.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.zcl.practice.R;
import com.zcl.practice.SystemUtil;

import java.util.Calendar;

public class CustomAnalogClock extends View {

    private Paint paint; // 画笔
    private Calendar calendar; // 日历对象,用于获取当前时间
    private float centerX, centerY; // 时钟中心点坐标
    private float radius; // 时钟半径
    private float hourHandLength, minuteHandLength, secondHandLength; // 时针、分针、秒针长度
    private int mColor;

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

    public CustomAnalogClock(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomAnalogClock(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr, 0);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAnalogClock, defStyleAttr,0);
        mColor = a.getColor(R.styleable.CustomAnalogClock_textColor, Color.BLACK);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            saveAttributeDataForStyleable(context, R.styleable.CustomAnalogClock, attrs, a, defStyleAttr, 0);
        }
        a.recycle();

        paint = new Paint();
        calendar = Calendar.getInstance();

        // 设置画笔的默认属性
        paint.setAntiAlias(true); // 抗锯齿
        paint.setStrokeCap(Paint.Cap.ROUND); // 圆形笔触
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // 计算时钟中心点和半径 支持padding设置
        centerX = (w - getPaddingLeft() - getPaddingRight()) / 2f + getPaddingLeft();
        centerY = (h - getPaddingTop() - getPaddingBottom()) / 2f + getPaddingTop();
        radius = Math.min(centerX-getPaddingLeft(), centerY-getPaddingTop()) - 10; // 留出一些边距

        // 设置指针长度
        hourHandLength = radius * 0.5f;
        minuteHandLength = radius * 0.7f;
        secondHandLength = radius * 0.9f;
    }


    // 设置wrap_content的默认宽 / 高值 支持wrap_content
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获取宽-测量规则的模式和大小
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        // 获取高-测量规则的模式和大小
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 设置wrap_content的默认宽 / 高值
        // 默认宽/高的设定并无固定依据,根据需要灵活设置
        // 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
        int mWidth = SystemUtil.dp2px(getContext(), 100);
        int mHeight = mWidth;

        // 当布局参数设置为wrap_content时,设置默认值
        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, mHeight);
            // 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
        } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, heightSize);
        } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(widthSize, mHeight);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 每次绘制前更新时间
        calendar = Calendar.getInstance();
        // 绘制表盘
        drawClockFace(canvas);
        // 绘制时针
        drawHourHand(canvas);
        // 绘制分针
        drawMinuteHand(canvas);
        // 绘制秒针
        drawSecondHand(canvas);
        // 绘制中心点
        drawCenterPoint(canvas);
        // 每秒刷新一次
        postInvalidateDelayed(1000);
    }

    private void drawClockFace(Canvas canvas) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(mColor);
        paint.setStrokeWidth(2);
        canvas.drawCircle(centerX, centerY, radius, paint);

        // 绘制刻度
        paint.setStrokeWidth(1);
        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) {
                paint.setStrokeWidth(2); // 整点刻度较粗
                paint.setTextSize(30);
                int result;
                if (i / 5 == 0) {
                    result = 12;
                } else {
                    result = i / 5;
                }
                String text = String.valueOf(result);
                Rect rect = new Rect();
                paint.getTextBounds(text, 0, text.length(), rect);
                float textWidth = rect.width();
                float textHeight = rect.height();
                canvas.drawText(text, centerX + (radius - 40) * (float) Math.cos(Math.toRadians(i * 6 - 90)) - textWidth / 2,
                        centerY + (radius - 40) * (float) Math.sin(Math.toRadians(i * 6 - 90)) + textHeight / 2, paint);
            } else {
                paint.setStrokeWidth(1); // 非整点刻度较细
            }
            canvas.drawLine(centerX + radius * (float) Math.cos(Math.toRadians(i * 6 - 90)),
                    centerY + radius * (float) Math.sin(Math.toRadians(i * 6 - 90)),
                    centerX + (radius - 10) * (float) Math.cos(Math.toRadians(i * 6 - 90)),
                    centerY + (radius - 10) * (float) Math.sin(Math.toRadians(i * 6 - 90)), paint);
        }
    }

    private void drawHourHand(Canvas canvas) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setColor(Color.BLACK);

        float hourAngle = (calendar.get(Calendar.HOUR) % 12 + calendar.get(Calendar.MINUTE) / 60f) * 30 - 90;
        float hourX = centerX + hourHandLength * (float) Math.cos(Math.toRadians(hourAngle));
        float hourY = centerY + hourHandLength * (float) Math.sin(Math.toRadians(hourAngle));
        canvas.drawLine(centerX, centerY, hourX, hourY, paint);
    }

    private void drawMinuteHand(Canvas canvas) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(3);
        paint.setColor(Color.BLACK);

        float minuteAngle = calendar.get(Calendar.MINUTE) * 6 - 90;
        float minuteX = centerX + minuteHandLength * (float) Math.cos(Math.toRadians(minuteAngle));
        float minuteY = centerY + minuteHandLength * (float) Math.sin(Math.toRadians(minuteAngle));
        canvas.drawLine(centerX, centerY, minuteX, minuteY, paint);
    }

    private void drawSecondHand(Canvas canvas) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(1);
        paint.setColor(Color.RED);

        float secondAngle = calendar.get(Calendar.SECOND) * 6 - 90;
        float secondX = centerX + secondHandLength * (float) Math.cos(Math.toRadians(secondAngle));
        float secondY = centerY + secondHandLength * (float) Math.sin(Math.toRadians(secondAngle));
        canvas.drawLine(centerX, centerY, secondX, secondY, paint);
    }

    private void drawCenterPoint(Canvas canvas) {
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.BLACK);
        canvas.drawCircle(centerX, centerY, 5, paint);
    }
}

// values - attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomAnalogClock">
        <attr name="textColor" format="color"/>
    </declare-styleable>
</resources>

其他

View的四个构造方法

    public FrameLayout(@NonNull Context context) {
        super(context);
    }

    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);
        saveAttributeDataForStyleable(context, R.styleable.FrameLayout,
                attrs, a, defStyleAttr, defStyleRes);

        if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
            setMeasureAllChildren(true);
        }

        a.recycle();
    }

AttributeSet attrs:在xml中定义的参数内容
int defStyleAttr:主题中优先级最高的属性
int defStyleRes: 优先级次之的内置于View的style(这里就是自定义View设置样式的地方),只有当defStyleAttr为0或者当前Theme中没有给defStyleAttr属性赋值时才起作用.
在android中的属性可以在多个地方进行赋值,涉及到的优先级排序为

  1. 布局xml中定义
  2. 布局xml中通过style定义
  3. 自定义View所在的Activity的Theme中指定style引用
  4. 构造函数中defStyleRes指定的默认值

Android自定义View:为什么自定义View wrap_content不生效?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值