Android 自定义View流程解析

2022-03-03  本文已影响0人  QiShare

1.简介

在开发中,View视图具有非常重要的作用,它是直接呈现给使用者的,因此向用户展示精美高效的View视图很有意义。Android系统提供了丰富的视图组件,如TextView、ImageView、Button等,还提供了RelativeLayout、LinearLayout、FrameLayout等组合组件,使用这些组件搭配能实现良好的视图效果。但是,有时候我们需要实现更加个性化和有特点的视觉效果,使用系统提供的组件就比较难满足这种需求了,此时自定义View视图便派上用场了,本文将主要分析继承View类实现自定义View视图的流程,去创建符合特定需求的自定义View视图。

2.自定义View流程

2.1 创建类并继承View

创建一个类,并继承View,本示例创建一个名为CustomView的类,需要实现其构造方法,为了在XML布局中使用自定义View的属性,至少需要提供一个参数包含Context和AttributeSet的构造方法,如下所示:

public class CustomView extends View {
    public CustomView(Context context) {
        this(context,null);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }
}

2.2 提供自定义属性

为了像系统提供的组件那样,可以在XML布局中设置视图组件的属性,需要提供自定义View的属性设置,在res/values路径下新建一个attrs.xml文件,并在其中编辑属性名和格式,常用的格式有string:字符串,boolean:布尔值,color:颜色值, dimension:尺寸值,enum:枚举值,flags:位,float:浮点值,fraction:百分数,integer整数值,reference:引用资源ID。示例如下:

<resources>
    <declare-styleable name="CustomView">
        <attr name="textContent" format="string|reference" />
        <attr name="textSize" format="dimension|reference" />
        <attr name="textColor" format="color|reference" />
        <attr name="circleColor" format="color|reference" />
    </declare-styleable>
</resources>

在XML布局中使用自定义属性,需要提供命名空间,命名空间的格式如:xmlns:[别名]="http://schemas.android.com/apk/res/[pacakge name],还有一种常用的命名空间:xmlns:app="http://schemas.android.com/apk/res-auto",示例如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <com.android.viewdemo.CustomView
        android:id="@+id/cv_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:textContent="Android"
        app:textSize="50sp"
        app:textColor="@color/teal_200"
        app:circleColor="@color/purple_500"/>
</RelativeLayout>

在XML布局中设置属性值后,接着便是在自定义的View中获取这些属性值,调用context.obtainStyledAttributes()返回TypedArray数组,TypedArray调用相应的方法获取属性值,如调用typedArray.getString(R.styleable.CustomView_textContent)获得字符串,TypedArray对象在调用之后要调用typedArray.recycle()回收资源,示例如下:

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView, 0, 0);
try {
    textContent = typedArray.getString(R.styleable.CustomView_textContent);
    textSize = typedArray.getDimensionPixelSize(R.styleable.CustomView_textSize, 50);
    textColor = typedArray.getColor(R.styleable.CustomView_textColor, 0);
    circleColor = typedArray.getColor(R.styleable.CustomView_circleColor, 0);
} finally {
    typedArray.recycle();
}

2.3 提供属性的getter和setter方法

自定义View的属性不仅可以在XML布局中设置,还应提供getter和setter方法,以便在代码中更改属性,在调用setter方法更改属性时,View的外观发生变化时需要调用invalidate()方法使当前的视图失效,进而触发onDraw()方法重绘视图,如果View的大小和形状发生了变化,则需要调用requestLayout()请求重新布局,需要注意的是invalidate()方法要在UI线程中调用,在非UI线程中调用postInvalidate(),示例如下:

public void setTextContent(String textContent) {
    this.textContent = textContent;
    //外观发生变化时,在UI线程中调用
    invalidate();
    //大小和形状发生了变化调用,非必要不调用,以提高性能
    requestLayout();
}

2.4 重写onMeasure()方法

此方法主要是用来控制View的大小,让父视图知道View希望的大小,在方法内计算得出希望View显示的大小后,调用setMeasuredDimension()方法将计算出的宽高传入,示例如下;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = 0;
    int height = 0;
    int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
    int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
    if (modeWidth == MeasureSpec.EXACTLY) {
        width = sizeWidth;
    } else if (modeWidth == MeasureSpec.AT_MOST) {
        width = Math.min(defaultWidth, sizeWidth);
    } else {
        width = defaultWidth;
    }

    int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
    int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
    if (modeHeight == MeasureSpec.EXACTLY) {
        height = sizeHeight;
    } else if (modeHeight == MeasureSpec.AT_MOST) {
        height = Math.min(defaultHeight, sizeHeight);
    } else {
        height = defaultHeight;
    }
    setMeasuredDimension(width, height);
}

2.5 重写onSizeChanged()方法

当视图的大小发生变化时,onSizeChanged()方法会被调用,onSizeChanged()方法会携带4个参数,分别是新的宽度、新的高度、旧的宽度、旧的高度,这对正确地绘制View至关重要,绘制需要的位置和尺寸等参数需要在此方法内进行计算,示例如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    textY = (float) h / 2;
    centerX = (float) w / 2;
    centerY = (float) h / 2;
    maxCircleRadius = (float) (w - 20) / 2;
}

2.6 初始化画笔Paint

绘制View需要用到画布Canvas和画笔Paint,Canvas负责处理绘制什么,如点、线、圆、矩形等,Paint负责处理如何绘制,如绘制的颜色、是否填充、透明度等,画布Canvas可以在重写onDraw()方法后获取,而画笔Paint则需要在初始化阶段新建一个或多个Paint对象,示例如下:

paintText = new Paint();
paintText.setAntiAlias(true);
paintText.setTextSize(textSize);
paintText.setColor(textColor);
paintText.setStyle(Paint.Style.FILL);

paintCircle = new Paint();
paintCircle.setAntiAlias(true);
paintCircle.setColor(circleColor);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(10);

2.7 重写onDraw()方法绘制View

绘制View是重要的一环,它将可见的界面呈现给使用者,重写onDraw()方法后,它将提供一个画布Canvas,它将和画笔Paint一起执行绘制,Canvas提供了丰富的绘制方法,如drawLine()绘制线段、drawText()绘制文本、drawPoint()绘制点、drawRect()绘制矩形等,传入计算好的参数和画笔,便可绘制出相应的图形,示例如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawText(textContent, 30, textY + 30, paintText);
    canvas.drawCircle(centerX, centerY, circleRadius, paintCircle);
}

2.8 响应用户手势操作

View还会经常与使用者进行交互,因此还需要响应和处理用户的手势操作,一般来说,需要重写onTouchEvent(MotionEvent event),在此方法内处理手势操作,常见的手势操作有按下、滑动、抬起等,在此方法内加上业务逻辑,示例如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:

            break;

        case MotionEvent.ACTION_MOVE:

            break;

        case MotionEvent.ACTION_UP:

            break;
    }
    return true;
}

此外,还可以借助GestureDetector类实现更多的手势检测,如双击、长按、滚动等。

2.9 添加动画效果

为了让自定义View更有吸引力和自然,还需要添加一些动画效果,这时候使用属性动画修改View的属性,可以产生动画效果,示例如下:

ObjectAnimator textAlpha = ObjectAnimator.ofInt(this, "textAlpha", 255, 50);
textAlpha.setDuration(2000);
textAlpha.setRepeatCount(ValueAnimator.INFINITE);
textAlpha.setRepeatMode(ValueAnimator.RESTART);
textAlpha.start();
textAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int animatedValue = (int) animation.getAnimatedValue();
        setTextAlpha(animatedValue);
    }
});

ObjectAnimator circle = ObjectAnimator.ofFloat(this, "circleRadius", 0.0f, maxCircleRadius);
circle.setDuration(2000);
circle.setRepeatCount(ValueAnimator.INFINITE);
circle.setRepeatMode(ValueAnimator.RESTART);
circle.start();
circle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float animatedValue = (float) animation.getAnimatedValue();
        setCircleRadius(animatedValue);
    }
});

2.10 对外提供回调接口

自定义View还应对外提供回调接口,以传递一些事件和数据,方便调用方处理相应的逻辑,常见的操作是在View内定义一些接口,在接口内部定义一些事件,并对外提供回调接口的方法,示例如下:

public interface OnCircleAnimationStartListener {
    void onCircleAnimationStart();
}

public void setOnCircleAnimationStartListener(OnCircleAnimationStartListener onCircleAnimationStartListener) {
    this.onCircleAnimationStartListener = onCircleAnimationStartListener;
}

cv_view.setOnCircleAnimationStartListener(new CustomView.OnCircleAnimationStartListener() {
    @Override
    public void onCircleAnimationStart() {

    }
});

3.总结

自定义View很有实用意义,在系统组件不能实现需求时,我们可以通过自定义View来达到目的。本文分析了实现自定义View的流程,包括自定义View属性、提供属性的getter和setter方法、重写onMeasure()、重写onSizeChanged()、初始化画笔Paint、重写onDraw()、响应用户手势操作、添加动画效果、对外提供回调接口。根据实际的需要,这些环节可能不需要都实现,或者增加别的环节。

上一篇下一篇

猜你喜欢

热点阅读