Android 自定义View-旋转小按钮

呃,什么是旋转小按钮?上图:

自定义这个View的原因是我需要一个能点击一下就能旋转显示正在刷新的小按钮,等刷新结束后在使它停止旋转并恢复到初始状态,并且这个View的字体大小,字体颜色,进度条的颜色等都可以自由配置。
自定义View包含以下几步:
1、自定义View的属性
2、在XML布局文件中使用自定义属性
2、在View的构造方法中获得我们配置的属性
3、重写onMesure
4、重写onDraw

自定义属性

1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml ,并在其中添加需要的属性,本例如下:


<resources>
    <declare-styleable name="RefreshView">
        <attr name="textSize" format="dimension">attr>
        <attr name="textColor" format="color">attr>
        <attr name="text" format="string">attr>
        <attr name="processColor" format="color">attr>
        <attr name="processWidth" format="dimension">attr>
    declare-styleable>
resources>

declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能又一个attr.xml文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称(引用来自【Android - 自定义View】之自定义View浅析);

在XML布局文件中使用自定义属性


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.sharenew.viewselfdefine.MainActivity">

    <com.sharenew.viewselfdefine.RefreshView
        android:layout_centerInParent="true"
        android:id="@+id/refreshview"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:padding="10dp"
        custom:text="刷新"
        custom:processWidth="3dp"
        custom:processColor="@android:color/holo_orange_light"
        custom:textColor="@android:color/holo_green_dark"
        custom:textSize="22sp"
        />
RelativeLayout>

我们的布局文件只有自定义的一个组件,它位于Activity的中央。为了使用自定义的属性,我们首先要声明自定的名称空间,在Gradle构建的工程中,它总是这样的:xmlns:custom=”http://schemas.android.com/apk/res-auto”
有了名称空间,我们就可以在该xml中使用自定的属性了。

在View的构造方法中获得我们配置的属性

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

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

    public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        /**
         * 获得我们所定义的自定义样式属性
         */

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++)
        {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.RefreshView_text:
                    mText = a.getString(attr);
                    break;
                case R.styleable.RefreshView_textColor:
                    // 默认颜色设置为黑色
                    mTextColor = a.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.RefreshView_textSize:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    mTextSize = (int) a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.RefreshView_processColor:
                    mProcessColor = a.getColor(attr,Color.YELLOW);
                    break;
                case R.styleable.RefreshView_processWidth:
                    mProcessWidth = (int) a.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_SP, 3, getResources().getDisplayMetrics()));
                    break;
            }

        }
        a.recycle();

        /**
         * 获得绘制文本的宽和高
         */
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        if(mText!=null){
            mTextBound = new Rect();
            mPaint.setStrokeWidth(1);
            mPaint.setTextSize(mTextSize);
            mPaint.getTextBounds(mText,0,mText.length(),mTextBound);
        }else {
            mTextBound = new Rect(0,0,30,30);
        }

        /**
         * 计算view的大小
         */
        mProcessBound = new RectF(0,0,mTextBound.width()+mProcessWidth,mTextBound.height()+mProcessWidth);
    }

我们重写了3个构造方法。在java代码中new出来的对象,默认使用的是一个参数的构造方法,使用布局文件创造的对象,默认使用的是的是两个参数的构造方法。关于为什么这两个构造函数最终都要调用到三个参数的构造函数,呃,这似乎已经成为了一种习惯…
注意:不管有没有使用自定义属性,都会默认调用两个参数的构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。

obtainStyledAttributes方法会从attrs中提取出自定义的属性,并将所有的属性存放在TypedArray 的一个对象中,这样通过简单的解析这个对象,就能得到所有的自定义属性的值。

重写onMesure

一个View的绘制流程是,首先调用onMeasure,然后才会调用onDraw。onMeasure用于测量这个view的大小。
对于自定的View,一定要处理WRAP_CONTENT的情况,不然配置WRAP_CONTENT会得到MATCH_PATENT的效果。

我们知道在ViewGroup中,给View分配的空间大小并不是确定的,有可能随着具体的变化而变化,而这个变化的条件就是传到specMode中决定的,specMode一共有三种可能:

MeasureSpec.EXACTLY:父视图希望子视图的大小应该是specSize中指定的。

MeasureSpec.AT_MOST:子视图的大小最多是specSize中指定的值,也就是说不建议子视图的大小超过specSize中给定的值。

MeasureSpec.UNSPECIFIED:我们可以随意指定视图的大小。

通过以上这些分析,可以知道视图最终的大小由父视图,子视图以及程序员根据需要决定,良好的设计一般会根据子视图的measureSpec设置合适的布局大小。

本例实现onMeasure如下:

    @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);
        int width;
        int height ;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            int maxTextSize = (mTextBound.width()>mTextBound.height())?mTextBound.width():mTextBound.height();
            width = maxTextSize + mProcessWidth*2+20;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            height = width;
        }
        viewWidth = width;
        viewHeight = height;
        mProcessBound.set(mProcessWidth+getPaddingLeft(),mProcessWidth+getPaddingTop(),width-mProcessWidth-getPaddingRight(),height-mProcessWidth-getPaddingBottom());
        setMeasuredDimension(width, height);
    }

重写onDraw

最后一步就是重写onDraw了。onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。
Paint类似一个画笔,Canvas类似一个画布。
画笔的粗细,实心还是空心等都是在Paint的实例中设置的,具体的绘制是在Canvas中绘制,因此Canvas中有很多的draw函数,比如画圆、画矩形等。
一下为本例实现的draw函数:


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.DKGRAY);
        mPaint.setStrokeWidth(mProcessWidth);
        canvas.drawArc(mProcessBound,0,360,false,mPaint);
        mPaint.setColor(mProcessColor);
        canvas.drawArc(mProcessBound,startAngle,swapArea,false,mPaint);
        if(mText!=null && mText!=""){
            mPaint.setColor(mTextColor);
            mPaint.setTextSize(mTextSize);
            mPaint.setStyle(Paint.Style.FILL);
            canvas.drawText(mText,(int)(viewWidth/2-mTextBound.width()/2),(int)(viewHeight/2+mTextBound.height()/2),mPaint);
        }
    }

注意,本例中,圆圈中的文字一定是居中的,Paint中有相关的方法可以测量字符串所占的像素大小,比如getTextBounds,measureText等。
更新动画的参数是在子线程中完成的,子线程通知UI线程重新绘制使用的是postInvalidate方法。为了启动和停止动画,我们必须提供start和stop接口,其实现如下:

    public void start(){
        startRun = true;
        startAngle = 0;
        swapArea = 60;
        new Thread(new Runnable() {
            boolean isSwapAreaReverse = false;
            @Override
            public void run() {
                while (startRun){
                    startAngle++;
                    if(isSwapAreaReverse){
                        swapArea--;
                        startAngle++;
                    }else {
                        swapArea++;
                    }
                    if(swapArea==360)isSwapAreaReverse = true;
                    if(swapArea==0)isSwapAreaReverse=false;
                    RefreshView.this.postInvalidate();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
    public void stop(){
        startRun = false;
        startAngle = 0;
        swapArea = 0;
        invalidate();
    }

以上便是自定义旋转小按钮的核心程序,需要全部代码可从这里下载:
Android 自定义View-旋转小按钮源码

你可能感兴趣的:(Android,UI)