android进阶4step1:Android动画处理与自定义View——自定义View

为什么要自定义控件

  1. 特定的显示风格
  2. 处理特有的用户交互
  3. 优化我们的布局
  4. 封装等...

如何自定义控件

  1. 自定义属性的声明与获取
  2. 测量onMeasure
  3. 绘制onDraw
  4. 状态的存储与恢复

步骤一、自定义属性声明与获取

  1. 分析需要的自定义属性
  2. 在res/valus/attrs.xml定义声明
  3. 在layout xml文件中进行使用
  4. 在View的构造方法中进行获取

实现步骤: 

1.新建TestView继承View 实现其构造方法(下面只实现一个)

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

2.在value目录下新建 attrs.xml文件(可以自定义名字) 自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--声明 name一般为自定义View的name-->
    <declare-styleable name="TestView">
        <!--字符串-->
        <attr name="test_string" format="string"></attr>
        <!--像素(px) 1dp=2px-->
        <attr name="test_dimension" format="dimension"></attr>
        <!--布尔值-->
        <attr name="test_boolean" format="boolean"></attr>
        <!--整形-->
        <attr name="test_integer" format="integer"></attr>
        <!--枚举类型:当变量只有几种固定的值时-->
        <attr name="test_enum" format="enum">
            <enum name="top" value="1"></enum>
            <enum name="bottom" value="2"></enum>
        </attr>

    </declare-styleable>
</resources>

3.在布局文件中使用自定义的布局

在根部局中添加 myview 一般是app 也可以自定义

    xmlns:myview="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:myview="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.demo.customview.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        myview:test_boolean="true"
        myview:test_enum="bottom"
        myview:test_integer="100"
        myview:test_dimension="100px"
        myview:test_string="哇哇哇"
        />

</RelativeLayout>

 4.在构造当中获取属性的值

方法1:直接通过typeArray 对象获取

public class TestView extends View {

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //加载在attr中自定义控件的属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TestView);
        //方法1:拿定义的属性值(属性name,默认值)
        boolean test_boolean = ta.getBoolean(R.styleable.TestView_test_boolean, false);
        float test_dimension = ta.getDimension(R.styleable.TestView_test_dimension, 100);
        String test_string = ta.getString(R.styleable.TestView_test_string);
        int test_integer = ta.getInteger(R.styleable.TestView_test_integer, 1);
        int test_enum = ta.getInt(R.styleable.TestView_test_enum, 1);
        Log.e("TAG", test_boolean + " , "
                + test_dimension + " , "
                + test_string + " , "
                + test_integer + " , "
                + test_enum);
        //回收掉
        ta.recycle();
    }

}

打印结果: 和在布局文件中设置的一致

12-10 12:55:06.213 1604-1604/? E/TAG: true , 100.0 , 哇哇哇 , 100 , 2

方法2:先获取在布局文件中一共设置属性的数目,如果有设置该属性则负责,否则使用原来定义的值

     //方法2:区别方法1:例如设置默认字符串 mText="def"
        //然后在 mText=ta.getString(R.styleable.TestView_test_string);
        //如果布局文件中没有设置该属性,则返回结果为null 原来的def就不见了
        //方法2可以实现如果没有设置,则显示默认的值
        //方法1如果想实现方法2的效果 也可以设置默认值

        //拿到在布局文件中设置的属性个数,没设置的不算
        int count = ta.getIndexCount();
        for (int i = 0; i < count; i++) {
            //拿到对应位置的属性名称
            int index = ta.getIndex(i);
            switch (index) {
                case R.styleable.TestView_test_string:
                    mText = ta.getString(R.styleable.TestView_test_string);
                    break;
            }

        }


        Log.e("TAG-2", test_boolean + " , "
                + test_dimension + " , "
                + mText + " , "
                + test_integer + " , "
                + test_enum);

布局文件修改:不定义test_string 属性

<com.demo.customview.TestView
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        android:background="@color/colorPrimaryDark"
        myview:test_boolean="true"
        myview:test_enum="bottom"
        myview:test_integer="100"
        myview:test_dimension="100px"
        />

 打印:

  • 方法2:会根据布局中是否设置了该属性的值,如果有则赋值,如果没有,则使用原来设置的值
  • 方法1:会自动更新ta.getString 的值,则为null
12-10 13:04:59.921 4315-4315/? E/TAG-1: true , 100.0 , null , 100 , 2
12-10 13:04:59.922 4315-4315/? E/TAG-2: true , 100.0 , def , 100 , 2

步骤二、自定义View的测量(onMeasure)

  1. EXACTLY(固定值 例如:100dp) , AT_MOST(最多不多过父布局 wrap_content) , UNSPECIFIED (不确定 滑动布局)
  2. MeasureSpec
  3. setMeasuredDimension
  4. requestLayout()(刷新重新测量)

上面函数实现了:

  • 接受父控件传入的高度 heightMeasureSpec
  • 通过MeasureSpec类获取mode 和size
  • 如果是布局设定为固定高度则直接返回size
  • 如果不是,如果是At_MOST 则需要取小于size的高度(不能大于父控件的size)返回

getMeasuredWidth和getWidth的区别

 

View的getWidth()和getMeasuredWidth()有什么区别吗?

View的高宽是由View本身和Parent容器共同决定的。
getMeasuredWidth()getWidth()分别对应于视图绘制的measure和layout阶段。

getMeasuredWidth()获取的是View原始的大小,也就是这个View在XML文件中配置或者是代码中设置的大小。getWidth()获取的是这个View最终显示的大小,这个大小有可能等于原始的大小,也有可能不相等。

比如说,在父布局的onLayout()方法或者该View的onDraw()方法里调用measure(0, 0),二者的结果可能会不同(measure中的参数可以自己定义)。

实现步骤:

1、重写OnMeasure方法

  • MeasureSpec.EXACTLY:精确模式,尺寸的值是多少,那么这个组件的长或宽就是多少
  • MeasureSpec.AT_MOST:最大模式:同时父控件给出一个最大空间,不能超过这个值
  • MeasureSpec.UNSPECIFIED:未指定模式,当前组件,可得到的空间不受限制
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        /**
         * 宽度测量
         * 1.拿到mode
         * 2.判断属于哪种mode
         * 3.得到具体的测量值
         */
        //拿到父控件传入宽度的mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        //设置测量宽度
        int width = 0;
        //如果设置的是确定的模式
        if (widthMode == MeasureSpec.EXACTLY) {
            //则测量的宽度为确定的宽度
            width = widthSize;
        } else {

            //所需要的宽度 如果有设置padding
            int needWidth = MeasureWidth() + getPaddingLeft() + getPaddingRight();
            if (widthMode == MeasureSpec.AT_MOST) {
                //取较小值 因为不能大于size
                needWidth = Math.min(needWidth, widthSize);
            } else {//否则就是UNSPECIFIED
                //你测量多大,就有多大
                width = needWidth;
            }

        }
        /**
         * 高度测量 同上
         */
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int height = 0;
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            int needHeight = MeasureHeight() + getPaddingBottom() + getPaddingTop();
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(needHeight, heightSize);
            } else {
                height = needHeight;
            }
        }
        //设置测量像素
        setMeasuredDimension(width, height);
    }


    /**
     * 返回空间的高
     * @return
     */
    private int MeasureHeight() {
        return 0;
    }
    /**
     * 返回空间的宽
     * @return
     */
    private int MeasureWidth() {

        return 0;
    }

步骤三、绘制onDraw

  • 1、绘制内容区域
  • 2、invalidate() , postInvalidate();
  • 3、Canvas.drawXXX
  • 4、translate、rotatescale、skew
  • 5、save()、restore()

具体步骤

重写onDraw方法

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        initPaint();
        //画一个圆形 以视图的中心为圆点 半径为宽度/2
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mPaint.getStrokeWidth() / 2, mPaint);
        //画一条直线
        canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
        //画一条直线
        canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
        //画字
           canvas.drawText(mText, 0, mText.length(), 0, getHeight() / 2, mPaint);

    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        //1.定义画笔
        mPaint = new Paint();
        //设置画笔的style 空心的
        mPaint.setStyle(Paint.Style.STROKE);
        //设置画笔颜色(红色)
        mPaint.setColor(0xFFFF0000);
        //设置画笔的大小
        mPaint.setStrokeWidth(6);
        //设置字体大小
        mPaint.setTextSize(100);
    }

效果:

 步骤4、状态的存储与恢复

首先把布局的背景色去掉来进行测试

我们知道当视图被中断的时候再回来会被重建(例如屏幕旋转 它会重新执行onCreate方法)

为了保存在重建之前要保存状态,等oncreate之后拿到上一次保存的状态 就实现了状态的存储和恢复

  • 1、onSaveInstanceState
  • 2、onRestoreInstanceState

 例子:没有进行状态存储之前 不会保存

重写以下方法:

   /**
     * 点击视图更新画面
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mText = "8888";
        //View 重绘 回调onDraw
        invalidate();
        //返回true代
        return true;
    }


    /**
     * 状态保存
     *
     * @return
     */
    @Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        //控件的字符串 之前定义的
        bundle.putString(KEY_TEXT, mText);
        //存父控件的状态
        bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
        return bundle;
    }

    /**
     * 状态恢复
     *
     * @param state
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle b = (Bundle) state;
            //恢复父类的状态
            Parcelable parcelable = b.getParcelable(INSTANCE);
            super.onRestoreInstanceState(parcelable);
            //恢复子类的状态
            mText = b.getString(KEY_TEXT);
            return;
        }
        super.onRestoreInstanceState(state);
    }

invalidate();

View(非容器类) 调用invalidate方法只会重绘自身,ViewGrounp调用则会重绘整个View树

之后:可以保存了

如果你发现还是不可以保存:那么你一定是忘记给控件添加id了!! 要在xml布局中添加id 因为系统是根据id来保存状态的

 结合上述四个步骤:实现案例


1、在attrs.xml文件中添加 自定义RoundProgressBar的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
     
    <declare-styleable name="RoundProgressBar">
        <attr name="color" format="color"></attr>
        <attr name="line_width" format="dimension"></attr>
        <attr name="radius" format="dimension"></attr>
        <!--也可以直接使用系统自带的属性-->
        <attr name="android:progress"></attr>
        <attr name="android:textSize"></attr>
    </declare-styleable>

    <!--声明-->
    <declare-styleable name="TestView">
  .....
    </declare-styleable>
</resources>

2.布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:myview="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.demo.customview.RoundProgressBar
        android:id="@+id/progressbar"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@color/colorPrimary"
        android:padding="10dp"
        android:layout_centerInParent="true" />

</RelativeLayout>

3.看一张图片

然后解决高度问题:  参考:Android自定义View绘制真正的居中文本
先了解一下Android是怎么样绘制文字的,这里涉及到几个概念,分别是文本的top,bottom,ascent,descent,baseline。 
Android自定义View绘制真正的居中文本 
Baseline是基线,在android中

文字的绘制都是从Baseline处开始的Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度)

Baseline往下至字符“最低处”的距离我们称之为descent(下坡度); 

leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;

Baseline是基线,Baseline以上是负值,以下是正值,因此 ascent是负值, descent是正值。

也可以通过

  • int a=mPaint.ascent() 拿到Ascent—Baseline的距离
  • int b=mPaint.desent()拿到Baseline—Desent的距离
  • b-a 即为Ascent——Desent的距离

4.代码实现(仔细看注解)

RoundProgressBar.java 自定义视图


import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/12/10 12:31<p>
 * <p>更改时间:2018/12/10 12:31<p>
 * <p>版本号:1<p>
 */
public class RoundProgressBar extends View {
    //定义成员变量
    private int mRadius; //半径
    private int mColor;//颜色
    private int mLineWidth;//线宽
    private int mTextSize;//字体大小
    private int mProgress;//进度

    private Paint mPaint;
    private RectF mProgressCicleRectf;
    private Rect bound;
    private int textHeight;
    private String text;

    public RoundProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //加载在attr中自定义控件的属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);
        mRadius = (int) ta.getDimension(R.styleable.RoundProgressBar_radius, dp2px(30));
        mColor = ta.getColor(R.styleable.RoundProgressBar_color, 0xffff0000);
        mLineWidth = (int) ta.getDimension(R.styleable.RoundProgressBar_line_width, dp2px(3));
        mTextSize = (int) ta.getDimension(R.styleable.RoundProgressBar_android_textSize, dp2px(16));
        mProgress = ta.getInt(R.styleable.RoundProgressBar_android_progress, 30);
        //初始化画笔
        initPaint();
        //回收掉
        ta.recycle();
    }

    /**
     * 将dp转成px的方法
     *
     * @param dpVal
     * @return
     */
    private float dp2px(int dpVal) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        /**
         * 宽度测量
         * 1.拿到mode
         * 2.判断属于哪种mode
         * 3.得到具体的测量值
         */
        //拿到父控件传入宽度的mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        //设置测量宽度
        int width = 0;
        //如果设置的是确定的模式
        if (widthMode == MeasureSpec.EXACTLY) {
            //则测量的宽度为确定的宽度
            width = widthSize;
        } else {

            //所需要的宽度 如果有设置padding
            int needWidth = MeasureWidth() + getPaddingLeft() + getPaddingRight();
            if (widthMode == MeasureSpec.AT_MOST) {
                //取较小值 因为不能大于size
                needWidth = Math.min(needWidth, widthSize);
            } else {//否则就是UNSPECIFIED
                //你测量多大,就有多大
                width = needWidth;
            }

        }
        /**
         * 高度测量 同上
         */
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int height = 0;
        if (heightMode == MeasureSpec.EXACTLY) {//指明宽度高度 width=xxdp
            height = heightSize;
        } else {
            int needHeight = MeasureHeight() + getPaddingBottom() + getPaddingTop();
            if (heightMode == MeasureSpec.AT_MOST) { //wrap_content时
                height = Math.min(needHeight, heightSize);
            } else {
                height = needHeight;
            }
        }
        //因为是圆形,假设用户传入宽高不等,肯定是不行的
        //所以取他们俩的最小值,保持宽高一定
        width = Math.min(width, height);
        //设置测量像素
        setMeasuredDimension(width, width);
    }


    /**
     * 返回控件需要的高
     *
     * @return
     */
    private int MeasureHeight() {
        return mRadius * 2;
    }

    /**
     * 返回控件需要的宽
     *
     * @return
     */
    private int MeasureWidth() {

        return mRadius * 2;
    }

    /**
     * 先进行onSizeChanged 再ondraw
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //内部圆形矩形区域
        mProgressCicleRectf = new RectF(0, 0, w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom());
        //拿到字的高度
        bound = new Rect();

    }

    /**
     * 画布绘制
     * ondraw方法中尽可能不要new对象和进行复杂的操作
     * 可以放到onSizeChanged中进行
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //空心圆
        mPaint.setStyle(Paint.Style.STROKE);
        //设置内部圆画笔的宽度
        mPaint.setStrokeWidth(mLineWidth * 1.0f / 4);
        //绘制圆
        int width = getWidth();
        int height = getHeight();
        //圆心x,y,半径r,画笔
        //半径要减去画笔的宽度的一半 和padding的距离
        canvas.drawCircle(width / 2, height / 2,
                width / 2 - getPaddingLeft() - mPaint.getStrokeWidth() / 2,
                mPaint);
        //绘制外部圆 圆弧
        //重写设置画笔宽度
        mPaint.setStrokeWidth(mLineWidth);
        canvas.save();
        //移动绘制坐标的圆点让(0,0)变成(getPaddingLeft(),getPaddingTop()) 在这里是(10,10)
        //就可以拿到圆所在的矩形区域了
        canvas.translate(getPaddingLeft(), getPaddingTop());
        //拿到角度
        float angle = mProgress * 1.0f / 100 * 360;
        //矩形的绘制 RectF 因为已经平移了 所以 可以将(10,10)当做新位置的初始坐标0,0,宽度,高度
        //参数1:所在的矩形区域(规定圆的范围) 参数2:开始的位置 参数3:结束的位置 参数4:是否画扇形 参数5:画笔
        canvas.drawArc(mProgressCicleRectf,
                0, angle, false, mPaint);
        canvas.restore();


        /**
         * 绘制中间文本 进度值
         */
        text = mProgress + "%";
        //让文本水平居中
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setTextSize(mTextSize);
        int y = getHeight() / 2;
        mPaint.getTextBounds(text, 0, text.length(), bound);
        textHeight = bound.height();
        //最主要是y的位置 是baseline(基线) 看图 已经在onSizeChanged中进行计算 避免在onDraw方法在new 对象
        //y的位置要加上文本的一半
        canvas.drawText(text, 0, text.length(), getWidth() / 2, y + textHeight / 2
                , mPaint);
        //特别说明:当字体是中文时 y的高度需要设置为 上移descent的1/2
        //y + textHeight / 2 - mPaint.descent()/2;


    }

    /**
     * 对外提供设置progress
     * 属性动画调用
     *
     * @param progress
     */
    public void setProgress(int progress) {
        mProgress = progress;
        //重绘视图
        invalidate();
    }

    public int getProgress() {
        return mProgress;
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        //1.定义画笔
        mPaint = new Paint();
        mPaint.setColor(mColor);
        //设置抗锯齿
        mPaint.setAntiAlias(true);
    }

    private static final String INSTANCE = "instance";
    private static final String KEY_PROGRESS = "key_progress";


    /**
     * 状态保存
     *
     * @return
     */
    @Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        //控件的字符串 之前定义的
        bundle.putInt(KEY_PROGRESS, mProgress);
        //存父控件的状态
        bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
        return bundle;
    }

    /**
     * 状态恢复
     *
     * @param state
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle b = (Bundle) state;
            //恢复父类的状态
            Parcelable parcelable = b.getParcelable(INSTANCE);
            super.onRestoreInstanceState(parcelable);
            //恢复子类的状态
            mProgress = b.getInt(KEY_PROGRESS);
            return;
        }
        super.onRestoreInstanceState(state);
    }
}

MainActivity.java

设置了属性动画,自动

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final View view = findViewById(R.id.progressbar);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //设置属性动画
                //对象属性自动赋值 get set 方法 自动赋值开始0 结束100
                ObjectAnimator.ofInt(view, "progress", 0, 100).setDuration(3000).start();
            }
        });
    }
}

完成啦

猜你喜欢

转载自blog.csdn.net/qq_17846019/article/details/84937614