如何自定义View

作者: Domon_Lee | 来源:发表于2017-06-12 12:02 被阅读143次

    首先奉上AndroodDeveloper的教程

    假设我们以自定义一个View,实现圆形的按钮功能。

    说一下简单的流程:

    • 继承View
    • 重写构造函数
    • 重写OnMeasure()方法
    • 重写OnDraw()方法
    • 配置XML
      我把配置XML放到最后不是因为需要最后去处理,而是它相对来说比较独立。

    继承View,重写构造函数

    首先重写View的构造函数

    private CustomView (Context context){
        this(context,null);
     }
    private CustomView(Context context, AttributeSet attrs){
        this(context,attrs,0);
    }
    private CustomView(Contxt context, AttributeSet attrs, int defStyleAttr){
        super(context,attrs,defStyleAttr);
     }
    

    重写OnMeasure()函数

    这一块着重的讲一下,之前我这里也不是特别的理解。我觉得在XML文件中其实已经将View 的尺寸宽高已经固定好了,何必在View中再次测量并设置么。同理可以再往上层分析下,若Google在父View中直接获取XML里面的尺寸岂不更好。

    在我们布局XML的时候,有两个属性wrap_contentmatch_parent。可以看到这两个属性并没有去告诉系统,我要多少尺寸的大小,而是描述了一种关系,即内容包裹填充父空间,因此在我们绘制到屏幕的过程中,就必须知道View的具体宽高,所以我们必须去处理尺寸。当View默认处理,无法满足我们需求的时候,就需要重写OnMeasure()函数了。

     @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int width = measure(widthMeasureSpec, 100);
            int height = measure(heightMeasureSpec, 100);
            
            if (width < height){
                height = width;
            }else{
                width = height;
            }
        
            setMeasuredDimension(width, height);
        }
    

    在这个函数中传了两个参数,这里需要注意的是参数是int型的,却包含了两个重要的信息:测量的模式以及测量的大小。Google将int数据的前两个bit用于区分不同的布局模式,后面三十个bit存放的是尺寸的数据。一般我们需要通过移位操作来获取数据,Android中的MeasureSpec中有两个函数getMode()getSize()就可以很方便的获取测量的模式和大小。

    这样恐怕你会有疑问,既然已经获取了View的Size了,那要Mode有何用?其实这里的Size只是父级View提供的参考大小而已。Mode分为下面三种:

    | 测量模式 | 英文 | 中文 |
    | UNSPECIFIED | The parent has not imposed any constraint on the child. It can be whatever size it wants | 父容器对当前View没有任何限制,当前View可以取任意尺寸
    | EXACTLY |   The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be | 父容器给子容器了确定的大小,无论子容器想要多大,它只能接收父容器给的大小
    | AT_MOST | The child can be as large as it wants up to the specified size | 子容器可以获得它想要的尺寸大小
    

    简单点来说,和warp_contentmatch_parten做下对比不难发现。

    match_parent--->EXACTLY。match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,即Size。

    wrap_content--->AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

    固定尺寸(如100dp)--->EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

        private int measure(int measureSpec, int defaultSize) {
            int result = defaultSize;
            int mode = MeasureSpec.getMode(measureSpec);
            int size = MeasureSpec.getSize(measureSpec);
            
            switch(mode){
                case MeasureSpec.EXACTLY:
                    result = size;
                    break;
                case MeasureSpec.AT_MOST:
                    result = size;
                    break;     
                case MeasureSpec.UNSPECIFIED:
                    result = defaultSize;
                    break;
                default:
                    break;
               }
            return result;
        }
    

    假设我们在XML中设置该控件的长宽属性都是match_parent,则效果如下

    重写OnDraw()方法

    上面我们通过OnMeasure()来设定了View的大小,接下来需要通过OnDraw()来绘制这个View的样子。

    这里需要注意下Canvas和Paint的区别,下面一段话说明Canvas确定了你在屏幕中所能展现的形状,而Paint用来定义具体的颜色,样式,字体等。

    Simply put, Canvasdefines shapes that you can draw on the screen, while Paint defines the color, style, font, and so forth of each shape you draw.

    OK,假设我们去绘制一个原谅色的原型,代码如下:

    @override
    protected void OnDraw(Canvas canvas){
        Super.onDraw(canvas);
        int r = getMeasureWidth() / 2;
        int x = getLeft() + r;
        int y = getTop() + r;
        
        Paint paint = new Paint();
        paint.setColor(Color.Green);
        canvas.drawCircle(x, y, r, paint);
    }
    

    效果如下:

    设置监听事件

    自定义XML属性

    如果我们需要给用户一些更加灵活的设置,就需设置一些属性。首先我们在res/values/styles.xml中声明我们自己的属性:

    <resources>
        <declare-styleable name="CostomView">
            <attr name="default_size" format="dimension"/>
        </declare-styleable>
    </resources>
    

    接着在布局文件中使用我们的声明的属性:

    <?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"
        android:orientation="vertical"
        tools:context="domon.cn.coustomerview.MainActivity">
    
        <domon.cn.coustomerview.View.RectView
            android:id="@+id/my_rv"
            android:layout_width="match_parent"
            android:background="#f2e"
            app:default_size="100dp"
            android:layout_height="100dp" />
        </LinearLayout>
    

    在引用自己的属性的时候,需要注意一下命名空间的问题,基本上我们的自定义View就已经好了。

    案例分析

    下面我代码家的一个自定义控件NumberProgressBar来简单分析一下。

    • 构造函数
    public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            default_reached_bar_height = dp2px(1.5f);
            default_unreached_bar_height = dp2px(1.0f);
            default_text_size = sp2px(10);
            default_progress_text_offset = dp2px(3.0f);
    
            //load styled attributes.
            final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar,
                    defStyleAttr, 0);
    
            mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color);
            mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color);
            mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color);
            mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size);
    
            mReachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_reached_bar_height, default_reached_bar_height);
            mUnreachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_unreached_bar_height, default_unreached_bar_height);
            mOffset = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_offset, default_progress_text_offset);
    
            int textVisible = attributes.getInt(R.styleable.NumberProgressBar_progress_text_visibility, PROGRESS_TEXT_VISIBLE);
            if (textVisible != PROGRESS_TEXT_VISIBLE) {
                mIfDrawText = false;
            }
    
            setProgress(attributes.getInt(R.styleable.NumberProgressBar_progress_current, 0));
            setMax(attributes.getInt(R.styleable.NumberProgressBar_progress_max, 100));
    
            attributes.recycle();
            initializePainters();
        }
    

    通过在构造函数中,获取在XML中设置的属性。

    • 重写OnMeasure()方法
    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
        }
    
        private int measure(int measureSpec, boolean isWidth) {
            int result;
            int mode = MeasureSpec.getMode(measureSpec);
            int size = MeasureSpec.getSize(measureSpec);
            int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
            if (mode == MeasureSpec.EXACTLY) {
                result = size;
            } else {
                result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
                result += padding;
                if (mode == MeasureSpec.AT_MOST) {
                    if (isWidth) {
                        result = Math.max(result, size);
                    } else {
                        result = Math.min(result, size);
                    }
                }
            }
            return result;
        }
    

    根据不同模式测量不同的尺寸

    • 重写OnDraw()方法
    @Override
        protected void onDraw(Canvas canvas) {
            if (mIfDrawText) {
                calculateDrawRectF();
            } else {
                calculateDrawRectFWithoutProgressText();
            }
    
            if (mDrawReachedBar) {
                canvas.drawRect(mReachedRectF, mReachedBarPaint);
            }
    
            if (mDrawUnreachedBar) {
                canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint);
            }
    
            if (mIfDrawText)
                canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint);
        }
    

    在OnDraw()中根据条件判断不同的绘制对象。我们就来看看calculateDrawRectFWithoutProgressText()这个方法。

    private void calculateDrawRectFWithoutProgressText() {
            mReachedRectF.left = getPaddingLeft();
            mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
            mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft();
            mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;
    
            mUnreachedRectF.left = mReachedRectF.right;
            mUnreachedRectF.right = getWidth() - getPaddingRight();
            mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
            mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
        }
    

    ReachedRectf是已经完成部分的矩形,UnReachedRectF是未完成的矩形。



    因此在绘制的时候,我们需要通过上下左右的位置坐标,将这个View画出来。

    相关文章

      网友评论

        本文标题:如何自定义View

        本文链接:https://www.haomeiwen.com/subject/vollqxtx.html