什么是自定义view

顾名思义就是自己定义的view 继承自view ,可以展现Android系统没有的效果,Android页面多姿多彩丰富的控件大多都是自定义view来实现的,在开发过程中也会经常使用自定义view来优化自己的页面

自定义view的步骤

前两步是一个通用模板,所有的自定义view都可以使用这种方法进行绘制,区别就在于第三步,每个view的绘制流程和方法都不同,这还需要点数学的知识

一. 自定义属性的声明与获取

        1 分析需要的自定义属性(例如字体颜色、线条宽度等)

        2 在res/values/下新建一个attrs.xml定义声明

        3 在layout文件中进行使用

        4 在view的构造方法中进行获取(自定义类继承自View)

这个attrs里面定义了两个View的类型 一个是bubbleView  还有一个是visualizerView

再里面的一层就是他们的名字和对应的类型了,宽高大小对应的就是dimension,color对应的类型就是color,enum是枚举类型,里面可以声明几个对应的属性

<resources>

<declare-styleable name="BubbleView">
        <attr name="arrowWidth" format="dimension" />
        <attr name="angle" format="dimension" />
        <attr name="arrowHeight" format="dimension" />
        <attr name="arrowPosition" format="dimension" />
        <attr name="bubbleColor" format="color" />
        <attr name="arrowLocation" format="enum">
            <enum name="left" value="0x00" />
            <enum name="right" value="0x01" />
            <enum name="top" value="0x02" />
            <enum name="bottom" value="0x03" />
        </attr>
 </declare-styleable>


 <declare-styleable name="visualizerView">
        <attr name="numColumns" format="integer" />
        <attr name="renderColor" format="color" />
        <attr name="renderRange">
            <enum name="top" value="0" />
            <enum name="bottom" value="1" />
            <enum name="both" value="2" />
        </attr>
        <attr name="renderType">
            <flag name="bar" value="0x1" />
            <flag name="pixel" value="0x2" />
            <flag name="fade" value="0x4" />
        </attr>
 </declare-styleable>


</resources>

二.测量OnMeasure

        1 获取宽高的模式:MeasureSpec.getMode 模式一共有三种,如下:

MeasureSpec.EXACTLY 在布局中指定了宽高 是一个确切的值(比如100dp 可以直接使用) MeasureSpec.ATMOST AT_MOST是最大的意思,这时需要测量自己控件需要的的宽高,如果大于父控件的,那么就用父控件的大小,如果小于父控件,就使用父控件大小 总结下来就是尽可能地给最大 但是不能超过父控件MeasureSpec.UNSPECIFIED 表示尽可能大的显示 在listview和scrollview中就是这样测量宽高的 因为是滚动控件,一般自己定义时很少用到 看scrollview的源码会看到 他的模式是获取最大的显示方式 childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding), MeasureSpec.UNSPECIFIED);

     用代码感受下获取宽高的模式该怎么使用 

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     
        int result=0;
        if (widthMode==MeasureSpec.EXACTLY){
            result=size;
        }else{

          //计算控件本身需要的宽高
            result=getNeedWidth()+getPaddingTop()+getPaddingBottom();

            if (widthMode==MeasureSpec.AT_MOST){
                result=Math.min(result,size);
            }
        }

        return result;

        2 setMeasuredDimension()  保存宽高

三.绘制OnDraw

1 绘制内容区域
        2 invalidate() postInvalidata()
        3 Canvas.drawXXX (可以drawCircle、Text、Points等)
        4 save() restore()

四 状态的存储与恢复

1 onSaveInstanceState
        2 onRestoreInstanceState

实战

自定义view流程就是这样,但是控件样式多种多样,实践才能加深印象,现在将这些方法用到实践中去吧!这里以一个进度条的加载为例子,先看效果:

processbar

1 创建一个新工程 

2 分析需要的自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="ProgressBar">
<!--       圆弧的颜色-->
       <attr name="innerBackground" format="color"/>
       <attr name="outerBackground" format="color"/>
<!--       圆弧的大小-->
       <attr name="roundWidth" format="dimension"/>
<!--       字体大小、颜色-->
       <attr name="progressTextSize" format="dimension"/>
       <attr name="progressTextColor" format="color"/>
       
   </declare-styleable>
</resources>

3 在main.xml文件中引用我们自己创建的progressBar这里的属性都是刚才自定义的  颜色和属性根据自己的需求给

<?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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <com.example.viewtest.ProgressBar
        android:id="@+id/progress_bar"
        app:progressTextSize="20sp"
        app:progressTextColor="#ff7788"
        app:innerBackground="#ee9933"
        app:outerBackground="#229933"
        app:roundWidth="5dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
<!--    测试按钮-->
    <Button
        android:id="@+id/test"
        android:layout_width="100dp"
        android:layout_height="50dp"
        android:layout_marginTop="30dp"
        android:layout_gravity="center"
        android:text="测试"
        android:background="@color/black"
        android:textColor="@color/white"/>

</LinearLayout>

4 创建一个类继承View 实现他的构造方法 然后编写里面的方法

public class ProgressBar extends View {

    private static final String TAG = "ProgressBar";

    //声明自定义属性
    private int mInnerBackground = Color.RED;
    private int mOuterBackground = Color.GREEN;
    private int mRoundWidth = 30;
    private int mProgressTextSize = 70;
    private int mProgressTextColor = Color.GREEN;

    //创建画笔
    private Paint mInnerPaint=new Paint();
    private Paint mOutterPaint = new Paint();
    private Paint mTextPaint = new Paint();

    //进度条的最大进度
    private int mMax=100;
    //当前进度
    private int mCurrentProgress=50;

    //new对象的时候调用该方法
    public ProgressBar(Context context) {
        super(context);
    }

    //在布局中使用   xml文件中使用
    public ProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    //  在layout中调用  自定义样式、有固定style的时候使用
    public ProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //获取自定义属性
        TypedArray array= context.obtainStyledAttributes(attrs,R.styleable.ProgressBar);
        mInnerBackground=array.getColor(R.styleable.ProgressBar_innerBackground,mInnerBackground);
        mOuterBackground=array.getColor(R.styleable.ProgressBar_outerBackground,mOuterBackground);
        mProgressTextColor= array.getColor(R.styleable.ProgressBar_progressTextColor,mProgressTextColor);
        mProgressTextSize= (int) array.getDimension(R.styleable.ProgressBar_progressTextSize,sp2px(mProgressTextSize));
        mRoundWidth= (int) array.getDimension(R.styleable.ProgressBar_roundWidth,dip2px(mRoundWidth));
        array.recycle();

//        mInnerPaint = new Paint();

    }

    private float sp2px(int sp) {
        return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics());
    }

    private float dip2px(int dip) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
    }

    public ProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }


    /**
     * onMeasure
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //拿到宽高
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        //存储view 的宽高 在这里取最小值 保证是一个正方形即可
        setMeasuredDimension(Math.min(width,height),Math.min(width,height));
    }

    /**
     * onDraw
     */

    @Override
    protected void onDraw(Canvas canvas) {

        mInnerPaint.setAntiAlias(true);//抗锯齿
        mInnerPaint.setColor(mInnerBackground);
        mInnerPaint.setStrokeWidth(mRoundWidth);//线条宽度
        mInnerPaint.setStyle(Paint.Style.STROKE);//空心

        mOutterPaint.setAntiAlias(true);//抗锯齿
        mOutterPaint.setColor(mOuterBackground);
        mOutterPaint.setStrokeWidth(mRoundWidth);//线条宽度
        mOutterPaint.setStyle(Paint.Style.STROKE);//空心

        mOutterPaint.setAntiAlias(true);//抗锯齿
        mTextPaint.setColor(mProgressTextColor);
        mTextPaint.setStrokeWidth(mRoundWidth);
        mTextPaint.setTextSize(mProgressTextSize);

//        super.onDraw(canvas);
        //先画内圆
        int center = getWidth()/2;
        Log.d(TAG, "onDraw: "+center);

       canvas.drawCircle(center,center,center-mRoundWidth/2,mInnerPaint);

        //再画外层圆
        RectF rectF=new RectF(0+mRoundWidth/2,0+mRoundWidth/2,getWidth()-mRoundWidth/2,getHeight()-mRoundWidth/2);
        if (mCurrentProgress==0){
            return;
        }
        float percent = (float) mCurrentProgress/mMax;
        canvas.drawArc(rectF,0,percent*360,false,mOutterPaint);

        //画进度文字
        String text=((int)(percent*100))+"%";
        Rect textBounds=new Rect();
        mTextPaint.getTextBounds(text,0,text.length(),textBounds);

        int x= getWidth()/2-textBounds.width()/2;
        Paint.FontMetricsInt fontMetrics=mTextPaint.getFontMetricsInt();
        int dy=(fontMetrics.bottom-fontMetrics.top)/2 - fontMetrics.bottom;
        int baseLineY=getHeight()/2+dy;

        canvas.drawText(text,x,baseLineY,mTextPaint);
    }


    public synchronized void setMax(int max){
        if (max<0){
            Log.d(TAG, "setMax: max<0 不合法"+max);
        }
        this.mMax=max;
    }

    public synchronized void setProgress(int process){
        if (process<0){
            Log.d(TAG, "setMax: max<0 不合法"+process);
        }
        this.mCurrentProgress=process;
        invalidate();//刷新
    }
}

5 在mainActivity中 调用 测试

public class MainActivity extends AppCompatActivity {

    private ProgressBar mProgressBar;

    private Button mTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initData();
    }

    private void initData() {
        mTest=findViewById(R.id.test);

        mProgressBar=(ProgressBar) findViewById(R.id.progress_bar);
        mProgressBar.setMax(1000);

        mTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                test();
            }
        });
    }

    private void test() {
        //值不断变化
        ValueAnimator animator= ObjectAnimator.ofFloat(0,1000);
        animator.setDuration(3000);
        animator.start();
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float progress = (float) animation.getAnimatedValue();
                mProgressBar.setProgress((int) progress);
            }
        });
    }
}

这样就OK啦

关于view的绘制还有很多要学习的东西,比如看到很多可以跳动的view,各种形状,还有和手机重力感应有关的有趣的view,后面将会继续学习。