美文网首页
Android控件架构与自定义控件

Android控件架构与自定义控件

作者: 张文靖同学 | 来源:发表于2017-08-31 00:16 被阅读28次

    前言

    最近在开发的路上越走越远了,每天在看各位大神公众号更新内容是自定义View的时候,一些小的内容有点模具,决定回过头来温习一下过往的内容。此篇也是根据android群英传来总结的一篇文章。

    1 Android控件架构

    Android的每个控件都是占一块矩形的区域,大致的分两类,继承View和ViewGroup,ViewGroup相当于一个容器,他可以管理多个字View,整个界面上的控件形成了一个树形结构,也就是我们常说的控件树,上层控件负责下层控件的测量和绘制,并且传递交互事件,通过findviewbyid()这个方法来获取,其实就是遍历查找,在树形图的顶部都有一个ViewParent对象,这就是控制核心,所有的交互管理事件都是由它统一调度和分配,从而进行整个视图的控制

    2 View的测量

    我们想要绘制一个View,首先还是得知道这个View的大小,系统是如何把他绘制出来的,在Android中,我们要想绘制一个View,就必须要知道这个View的大小,然后告诉系统,这个过程在onMeasure()中进行。

    Android给我们提供了一个设计短小精悍的类——MeasureSpec类,通过他来帮助我们测量View, MeasureSpec是一个32位的int值,其中高2位为测量模式,低30为测量的大小,在计算中使用位运算时为了提高并且优化效率
    三种测量模式如下:

    1. EXACTLY 精确值模式
      表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
    2. AT_MOST最大值模式
      控件的尺寸不超过父控件允许的最大尺寸即可
    3. UNSPECIFIED
      表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

    一份模板代码:

    private int measureWidth(int measureSpec) {
            int result = 0;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            if (specMode == MeasureSpec.EXACTLY) {
                result = specSize;
    
            } else {
                result = 200;
                if (specMode == MeasureSpec.AT_MOST) {
                    result = Math.min(result, specSize);
                }
            }
            return result;
        }
    

    3 View的绘制

    Canvas顾名思义,画布的意思,而onDraw()就一个参数,就是Canvas了,我们要在其他地方绘制的话,就需要new对象了

    Canvas canvas = new Canvas(Bitmap);
    
    //绘制直线
    canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);
    //绘制矩形
    canvas.drawRect(float left, float top, float right, float bottom, Paint paint);
    //绘制圆形
    canvas.drawCircle(float cx, float cy, float radius, Paint paint);
    //绘制字符
    canvas.drawText(String text, float x, float y, Paint paint);
    //绘制图形
    canvas.drawBitmap(Bitmap bitmap, float left, float top, Paint paint);
    

    4 ViewGroup的测量

    前面也说过,ViewGroup是老大,他是用来管理View的,包括View的大小什么的,当我们的ViewGroup大小是包裹内容的时候,实际上ViewGroup会遍历所有的子View,来获取View的大小,从而决定自身的大小,而在其他模式下,会通过具体的值来自定自身的大小

    ViewGroup遍历所有的View会调用所有的View的onMeasure()方法来获取测量结果,当子View测量完毕之后,,就需要将子View放在合适的地方,这部分是由onLayout()来进行的,在我们自定义ViewGroup的时候,一般都要重写onLayout()方法控制子View显示位置的逻辑,同样,如果需要wrap_content属性,那就必须重写onLayout()方法了,这点和View是相同的

    5 ViewGroup的绘制

    ViewGroup在一般情况下是不会绘制的,因为他本身没有需要绘制的东西,如果不是指定ViewGroup的背景颜色,他连onDraw()都不会调用,但是ViewGroup会使用dispatchDraw()来绘制其他子View,其过程同样是遍历所哟普的子View,并调用子View的绘制方法来完成绘制的

    6 自定义View

    自定义View一直是个难点,Android自带的控件很难满足我们的需求,所欲我们需要重写控件或者自定义一个View,但是一般强大的View,都还是存在少许的bug的,而且现在Android ROM的多样性,适配问题也越来越麻烦了,当然,自定义View你熟悉之后,可以了解系统绘制控件的原理,而且能让你的APP更加美观,强大。

    在View中通常有以下比较重要的回调方法
    首先,我们应该了解一下比较重要的回调方法:

    • onDraw() 绘制View的显示内容
    • onMeasure() 使用此方法时多是该View支持wrap_content属性
    • onFinishInflate() 从XML加载组件后回调
    • onSizeChanged() 组件大小改变后回调
    • onMeasure(int widthMeasureSpec, int heightMeasureSpec)回调该方法进行测量
    • onLayout(boolean changed, int left, int top, int right, int bottom)回调该方法确定显示位置
    • onTouchEvent(MotionEvent event) 监听到触摸事件时的回调

    以上就是几种常用的回调的方法.上面的方法并不需要全部写出来,看个人需要,一般我们实现自定义控件有三种方法

    • 对现有的控件进行扩展
    • 通过组件来实现新的控件
    • 重写View来实现全新的控件

    6.1 对现有控件进行扩展
    这是一个我们十分常用的一个方法,用来对现有的控件进行扩展,比如TextView需要渐变啊什么的,挺常用的,这里我们就来写一个小栗子,我們先來看下效果

        public class MyTextView extends TextView {
        private Paint mPaint1, mPaint2;
    
        public MyTextView(Context context) {
            super(context);
            initView();
        }
    
        public MyTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        public MyTextView(Context context, AttributeSet attrs,
                          int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView();
        }
    
        private void initView() {
            mPaint1 = new Paint();
            mPaint1.setColor(getResources().getColor(
                    android.R.color.holo_blue_light));
            mPaint1.setStyle(Paint.Style.FILL);
            mPaint2 = new Paint();
            mPaint2.setColor(Color.YELLOW);
            mPaint2.setStyle(Paint.Style.FILL);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 绘制外层矩形
            canvas.drawRect(
                    0,
                    0,
                    getMeasuredWidth(),
                    getMeasuredHeight(),
                    mPaint1);
            // 绘制内层矩形
            canvas.drawRect(
                    10,
                    10,
                    getMeasuredWidth() - 10,
                    getMeasuredHeight() - 10,
                    mPaint2);
            canvas.save();
            // 绘制文字前平移10像素
            canvas.translate(10, 0);
            // 父类完成的方法,即绘制文本
            super.onDraw(canvas);
            canvas.restore();
        }}
    

    一个稍微复杂的TextView:


    这个可以利用LinearGradient,Shader,Matrix,来完成,来实现一个闪闪发光的闪动效果,我们充分的利用Shader渲染器,来设置一个不断变化的LinearGradient,首先我们要在onSizeChanged()方法中完成一些初始化操作

    public class CoolTextView extends TextView {
        private int mViewWidth;
        //初始化画笔
        private Paint mPaint;
        //渲染器
        private LinearGradient mLinearGradient;
        //矩阵
        private Matrix matrix;
    
        private int mTranslate;
    
    
    
        public CoolTextView(Context context) {
            super(context);
        }
    
        public CoolTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mPaint=new Paint();
            if (mViewWidth==0){
                mViewWidth=getMeasuredWidth();
                if (mViewWidth>0){
                    //获取当前TextView的画笔
                    mPaint=getPaint();
                    //渲染器
                    mLinearGradient=new LinearGradient(0,0,mViewWidth,0,
                            new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
                            null, Shader.TileMode.CLAMP);
                    mPaint.setShader(mLinearGradient);
                    matrix=new Matrix();
                }
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (matrix!=null){
                //修改可以改变显示的速度
                mTranslate+=mViewWidth/10;
                if (mTranslate>1*mViewWidth){
                    mTranslate=-mViewWidth;
                }
                matrix.setTranslate(mTranslate,0);
                mLinearGradient.setLocalMatrix(matrix);
                //每隔100毫秒闪动一下
                postInvalidateDelayed(100);
            }
        }
        /*
        LinearGradient参数:
        float x0: 渐变起始点x坐标
        float y0:渐变起始点y坐标
        float x1:渐变结束点x坐标
        float y1:渐变结束点y坐标
        int[] colors:颜色 的int 数组
        float[] positions: 相对位置的颜色数组,可为null,  若为null,可为null,颜色沿渐变线均匀分布
        Shader.TileMode tile: 渲染器平铺模式*/
    }
    

    **6.2 复合控件 **
    创建一个复核人控件可以很好的创建出具有重要功能的控件集合,这种方式经常需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组成一个新的合适的控件,通过这种方式创建的控件,我们一般都会给他指定的一些属性,让他具有更强的扩展性,下面就以一个TopBar为例子,讲解如何创建复合控件

    1. 定义属性
      我们需要给他定义一些属性,这样的话,我们需要在values下新建一个attrs.xml文件
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="TopBar">
            <attr name="title" format="string" />
            <attr name="titleTextSize" format="dimension" />
            <attr name="titleTextColor" format="color" />
            <attr name="leftTextColor" format="color" />
            <attr name="leftBackground" format="reference|color" />
            <attr name="leftText" format="string" />
            <attr name="rightTextColor" format="color" />
            <attr name="rightBackground" format="reference|color" />
            <attr name="rightText" format="string" />
        </declare-styleable>
    </resources>
    

    我们在代码中是可以用< declare-styleable >标签去声明一些属性的,然后name相当于ID让我们的类可以找到,,确定好之后,我们新建一个类,就叫TopBarView

    package com.zc.demo;
    
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.drawable.Drawable;
    import android.util.AttributeSet;
    import android.view.ViewGroup;
    
    /**
     * TopBar
     * Created by Zc on 17/8/31.
     */
    public class TopBarView extends ViewGroup {
    
        private int mLeftTextColor;
        private Drawable mLeftBackground;
        private String mLeftText;
        private int mRightTextColor;
        private Drawable mRightBackgroup;
        private String mRightText;
        private float mTitleSize;
        private int mTitleColor;
        private String mTitle;
        //带参构造方法
        public TopBarView(Context context, AttributeSet attrs) {
            super(context, attrs);
    
            //通过这个方法,你可以从你的attrs.xml文件下读取读取到的值存储在你的TypedArray
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
            //读取出相应的值设置属性
            mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
            mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
            mLeftText = ta.getString(R.styleable.TopBar_leftText);
            mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
            mRightBackgroup = ta.getDrawable(R.styleable.TopBar_rightBackground);
            mRightText = ta.getString(R.styleable.TopBar_rightText);
            mTitleSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
            mTitleColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
            mTitle = ta.getString(R.styleable.TopBar_title);
            //获取完TypedArray的值之后,一般要调用recyle方法来避免重复创建时候的错误
            ta.recycle();
        }
    
    

    6.3 重写View来实现全新的控件
    当我们Android原生的控件不满足的话,我们可以继承原来的控件修改,也可以组合起来使用,更加可以继承View创建一个新的控件View

    效果如图所示
    
    public class CircleProgressView extends View {
    
        private int mMeasureHeigth;
        private int mMeasureWidth;
    
        private Paint mCirclePaint;
        private float mCircleXY;
        private float mRadius;
    
        private Paint mArcPaint;
        private RectF mArcRectF;
        private float mSweepAngle;
        private float mSweepValue = 66;
    
        private Paint mTextPaint;
        private String mShowText;
        private float mShowTextSize;
    
        public CircleProgressView(Context context, AttributeSet attrs,
                                  int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public CircleProgressView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public CircleProgressView(Context context) {
            super(context);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec,
                                 int heightMeasureSpec) {
            mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);
            mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);
            setMeasuredDimension(mMeasureWidth, mMeasureHeigth);
            initView();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            // 绘制圆
            canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
            // 绘制弧线
            canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
            // 绘制文字
            canvas.drawText(mShowText, 0, mShowText.length(),
                    mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
        }
    
        private void initView() {
            float length = 0;
            if (mMeasureHeigth >= mMeasureWidth) {
                length = mMeasureWidth;
            } else {
                length = mMeasureHeigth;
            }
    
            mCircleXY = length / 2;
            mRadius = (float) (length * 0.5 / 2);
            mCirclePaint = new Paint();
            mCirclePaint.setAntiAlias(true);
            mCirclePaint.setColor(getResources().getColor(
                    android.R.color.holo_blue_bright));
    
    
            mArcRectF = new RectF(
                    (float) (length * 0.1),
                    (float) (length * 0.1),
                    (float) (length * 0.9),
                    (float) (length * 0.9));
            mSweepAngle = (mSweepValue / 100f) * 360f;
            mArcPaint = new Paint();
            mArcPaint.setAntiAlias(true);
            mArcPaint.setColor(getResources().getColor(
                    android.R.color.holo_blue_bright));
            mArcPaint.setStrokeWidth((float) (length * 0.1));
            mArcPaint.setStyle(Style.STROKE);
    
            mShowText = setShowText();
            mShowTextSize = setShowTextSize();
            mTextPaint = new Paint();
            mTextPaint.setTextSize(mShowTextSize);
            mTextPaint.setTextAlign(Paint.Align.CENTER);
        }
    
        private float setShowTextSize() {
            this.invalidate();
            return 50;
        }
    
        private String setShowText() {
            this.invalidate();
            return "Panda_Program";
        }
    
        public void forceInvalidate() {
            this.invalidate();
        }
    
        public void setSweepValue(float sweepValue) {
            if (sweepValue != 0) {
                mSweepValue = sweepValue;
            } else {
                mSweepValue = 25;
            }
            this.invalidate();
        }
    }
    

    在来一个例子 效果如下所示

    模拟音频输入
    public class VolumeView extends View {
    
        private int mWidth;
        private int mRectWidth;
        private int mRectHeight;
        private Paint mPaint;
        private int mRectCount;
        private int offset = 5;
        private double mRandom;
        private LinearGradient mLinearGradient;
    
        public VolumeView(Context context) {
            super(context);
            initView();
        }
    
        public VolumeView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        public VolumeView(Context context, AttributeSet attrs,
                          int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView();
        }
    
        private void initView() {
            mPaint = new Paint();
            mPaint.setColor(Color.BLUE);
            mPaint.setStyle(Paint.Style.FILL);
            mRectCount = 12; //条形数量
        }
    
        //条形的渐变色
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mWidth = getWidth();
            mRectHeight = getHeight();
            mRectWidth = (int) (mWidth * 0.6 / mRectCount);
            mLinearGradient = new LinearGradient(
                    0,
                    0,
                    mRectWidth,
                    mRectHeight,
                    Color.YELLOW,
                    Color.BLUE,
                    Shader.TileMode.CLAMP);
            mPaint.setShader(mLinearGradient);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            for (int i = 0; i < mRectCount; i++) {
                mRandom = Math.random();
                float currentHeight = (float) (mRectHeight * mRandom);
                canvas.drawRect(
                        (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
                        currentHeight,
                        (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),
                        mRectHeight,
                        mPaint);
            }
            //300毫秒重新绘制一次
            postInvalidateDelayed(300);
        }
    }
    

    7 自定义ViewGroup

    这个管理子View的管理者,我们来定义一下,通常我们自定义ViewGroup是需要onMeasure()来测量的,然后重写onLayout()来确定位置,重写onTouchEvent()来相应事件
    接下来制作一个仿ScrollView的效果并且增加粘性事件。

    public class MyScrollView extends ViewGroup {
    
        private int mScreenHeight;
        private Scroller mScroller;
        private int mLastY;
        private int mStart;
        private int mEnd;
    
        public MyScrollView(Context context) {
            super(context);
            initView(context);
        }
    
        public MyScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView(context);
        }
    
        public MyScrollView(Context context, AttributeSet attrs,
                            int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView(context);
        }
    
        private void initView(Context context) {
            WindowManager wm = (WindowManager) context.getSystemService(
                    Context.WINDOW_SERVICE);
            DisplayMetrics dm = new DisplayMetrics();
            wm.getDefaultDisplay().getMetrics(dm);
            mScreenHeight = dm.heightPixels;
            mScroller = new Scroller(context);
        }
    
        @Override
        protected void onLayout(boolean changed,
                                int l, int t, int r, int b) {
            int childCount = getChildCount();
            // 设置ViewGroup的高度
            MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
            mlp.height = mScreenHeight * childCount;
            setLayoutParams(mlp);
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != View.GONE) {
                    child.layout(l, i * mScreenHeight,
                            r, (i + 1) * mScreenHeight);
                }
            }
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec,
                                 int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int count = getChildCount();
            for (int i = 0; i < count; ++i) {
                View childView = getChildAt(i);
                measureChild(childView,
                        widthMeasureSpec, heightMeasureSpec);
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mLastY = y;
                    mStart = getScrollY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (!mScroller.isFinished()) {
                        mScroller.abortAnimation();
                    }
                    int dy = mLastY - y;
                    if (getScrollY() < 0) {
                        dy = 0;
                    }
                    if (getScrollY() > getHeight() - mScreenHeight) {
                        dy = 0;
                    }
                    scrollBy(0, dy);
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_UP:
                    int dScrollY = checkAlignment();
                    if (dScrollY > 0) {
                        if (dScrollY < mScreenHeight / 3) {
                            mScroller.startScroll(
                                    0, getScrollY(),
                                    0, -dScrollY);
                        } else {
                            mScroller.startScroll(
                                    0, getScrollY(),
                                    0, mScreenHeight - dScrollY);
                        }
                    } else {
                        if (-dScrollY < mScreenHeight / 3) {
                            mScroller.startScroll(
                                    0, getScrollY(),
                                    0, -dScrollY);
                        } else {
                            mScroller.startScroll(
                                    0, getScrollY(),
                                    0, -mScreenHeight - dScrollY);
                        }
                    }
                    break;
            }
            postInvalidate();
            return true;
        }
        private int checkAlignment() {
            int mEnd = getScrollY();
            boolean isUp = ((mEnd - mStart) > 0) ? true : false;
            int lastPrev = mEnd % mScreenHeight;
            int lastNext = mScreenHeight - lastPrev;
            if (isUp) {
                //向上的
                return lastPrev;
            } else {
                return -lastNext;
            }
        }
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {
                scrollTo(0, mScroller.getCurrY());
                postInvalidate();
            }
        }
    }
    

    8 事件拦截机制分析
    这章讲的是一个事件拦截机制的一些基本概念,,当Android系统扑捉到用户的各种输入事件之后,如何准确的传递给真正需要这个事件的控件尼?其实Android提供了一套非常完善的事件传递,处理机制,来帮助开发者完成准确的事件分配和处理要想了解拦截机制,我们首先要知道什么事触摸事件,一般MotionEvent提供的手势,我们常用的几个DOWN,UP,MOVE什么的在MotionEvent中封装了很多东西,比如获取坐标点event.getX()和getRawX()获取
    一般ViewGroup我们需要重写三个方法

    @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }
    

    而View则只要重写两个方法

     @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }
    

    小结

    至此空间架构与自定义控件基础内容就完成了,日后碰见有趣的自定义控件,我会更新在我的博客上,欢迎浏览

    相关文章

      网友评论

          本文标题:Android控件架构与自定义控件

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