Android自定义控件

来源:互联网 发布:软件接口测试方法 编辑:程序博客网 时间:2024/06/03 07:31

1)Android控件

1)控件树

在Android中每个控件都占有一块矩形区域,控件一般分两类,View和ViewGroup,ViewGroup作为父控件可以包含多个子控件,并管理其包含的View控件。通过ViewGroup整个界面上的控件形成了一个树形结构,也就是常说的控件树,上层控件负责下层控件的测量和绘制,并传递交互事件,通常在Activity中使用findViewById()方法,就是在控件树中以树的深度优先遍历来查找对应元素。每个树的顶部都有一个DecorView对象,所有的交互事件都由它来统一调度和分配,从而可以对整个视图进行整体控制。下图展示了其树形结构.

这里写图片描述

2)UI界面架构图

这里写图片描述

Activity:基本的页面单元,Activity包含一个Window,window上可以绘制各种view

Window:表示顶层窗口,管理界面的显示和事件的响应;每个Activity均会创建一个

PhoneWindow对象,是Activity和整个View系统交互的接口,是Window的具体实现。PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。

DecorView:是Window中View的RootView,设置窗口属性;该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即“修饰”的意思,DecorView就是对普通的FrameLayout进行了一定的修饰,比如添加一个通用的Titlebar,并响应特定的按键消息等。

ViewRoot:它并不是一个View类型,而是一个Handler。
它的主要作用如下:
A. 向DecorView分发收到的用户发起的event事件,如按键,触屏,轨迹球等事件;
B. 与WindowManagerService交互,完成整个Activity的GUI的绘制。

3)绘制流程

整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为

这里写图片描述

根据之前设置的状态,判断是否需要重新计算视图大小(measure)、是否重新需要安置视图的位置(layout)、以及是否需要重绘 (draw)。

2)View的测量

在Android中控件不会无缘无故出现在屏幕上,其内部必然经过大量的测量,计算布局和绘制,才会显示在界面上,而作为自定义控件中最重要的步骤测量也是最重要一环。

先来了解下几个重要概念

MeasureSpec 它是来帮助我们测量View的一个类,MeasureSpec是一个32位的int值,其中高2位为测量模式,低30位为测量的大小,通过这个可以获取测量模式和大小。

三种测量模式

  • EXACTLY

    精确测量模式,当我们在布局中将宽高设置为具体数值或match_parent(占据父View的大小)时,系统使用这种测量模式。
  • AT_MOST

    最大值模式,当控件的宽高设置为wrap_content时,此时控件大小随着子控件或内容的变化而变化,此时控件大小只要不超过父控件运行的大小就可以了。
  • UNSPECIFIED

    不指定其大小测量模式,View想多大就多大,通常在绘制自定义View时使用,一般不用,可以不用管。

要测量,需要重写onMeasure()

这里写图片描述

看下一般测量流程的模板代码

这里写图片描述

可以看到先通过 MeasureSpec对象中取出测量的模式和大小,通过测量的模式,给出不同的测量值。当SpecMode为EXACTLY,就使用用户给定的测量大小,其他两种,需要给定一个默认的测量大小,当为AT_MOST时,需要取出系统测量值与默认值的最小值,这就是其大致测量模式。

3)View的绘制

当测量好了,我们就可以绘制了,我们需要重写onDraw()方法,

这里写图片描述

具体的绘制流程还是需要看不同的需求的,这后面再说。

4)ViewGroup的测量

ViewGroup管理其子View,其中负责子View的显示大小。

测量:ViewGroup的大小为wrap_content(即AT_MOST)时,ViewGroup对子View遍历,获取所有子View的大小(调用子View的Measure方法获取测量结果),从而决定自己的大小。其他模式下会通过具体的指定值来设置大小。

Layout过程:测量后需要将子View放到合适的位置。遍历子View的Layout方法,并制定其具体显示的位置,从而来具体决定其布局位置(可以重写onLayout()来控制子View的显示位置的逻辑,如果需要支持wrap_content属性,需重写onMeasure())。

5)ViewGroup的绘制

ViewGroup通常情况是不需要绘制的。 当指定背景颜色时,onDraw方法会被调用。 ViewGroup可以通过dispatchDraw方法来(遍历调用子View的onDraw方法)绘制子View。

6)自定义View

自定义View中通常用到的回调方法

  • onFinishInflate(): xml加载完毕之后回调

  • onSizeChanged(): 组件大小改变之后回调。

  • onMeasure(): 组件测量

  • onLayout(): 确定ViewGroup中子view的位置

  • onTouchEvent(): 监听触摸事件回调

  • onDraw(): 最常用,绘制View

一般有三种方法来实现自定义的控件:

1) 对现有组件进行拓展

2) 通过组合来实现新的控件

3) 重写View来实现全新的控件

7)重写View来实现全新的控件

前面两种就不多说了,蛮容易懂得,看下《Android群英传》就可以懂了

结合案例来说下使用

1)弧线展示图

package com.chen.demo.view;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.RectF;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.view.View;import android.view.WindowManager;/** * Created by chenxiaokang on 2016/12/1. */public class MyView extends View {    private int length;    private int mCircleXY;    private float mRadius;    private RectF mArcRectF;    private int mSweepAngle;    private Paint mPaint;    private Paint mArcPaint;    private Paint mTextPaint;    private String mText;    public MyView(Context context) {        this(context, null);    }    public MyView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        mSweepAngle = 187;        //绘制圆形的画笔        mPaint = new Paint();        mPaint.setStyle(Paint.Style.FILL);        mPaint.setAntiAlias(true);        mPaint.setColor(Color.GREEN);        //绘制弧形的画笔        mArcPaint = new Paint();        mArcPaint.setAntiAlias(true);        mArcPaint.setStyle(Paint.Style.STROKE);        mArcPaint.setStrokeWidth(24);        mArcPaint.setColor(Color.RED);        //绘制文字的画笔        mTextPaint = new Paint();        mTextPaint.setAntiAlias(true);        mTextPaint.setColor(Color.BLUE);        mTextPaint.setTextSize(100);        mTextPaint.setTextAlign(Paint.Align.CENTER);        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics metrics = new DisplayMetrics();        wm.getDefaultDisplay().getRealMetrics(metrics);        length = Math.min(metrics.widthPixels, metrics.heightPixels) - 20;        //中间的圆        mCircleXY = length/2;        mRadius = length*0.5f/2;        //弧线        mArcRectF = new RectF(length*0.1f, length*0.1f, length*0.9f, length*0.9f);        mText = "全自定义View";    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(length, length);    }    @Override    protected void onDraw(Canvas canvas) {        //绘制内部圆        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mPaint);        //绘制弧线        canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);        //绘制文字        int textHeight = (int) (mTextPaint.descent()-mTextPaint.ascent());        canvas.drawText(mText, 0, mText.length(), mCircleXY, mCircleXY+textHeight/4, mTextPaint);    }}

效果:

这里写图片描述

我们通过自定义View绘制了三个部分,内部圆,弧线,和文字,绘制了一个全新的控件。

2)音频图

package com.chen.demo.view;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.LinearGradient;import android.graphics.Paint;import android.graphics.Shader;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.view.View;import android.view.WindowManager;/** * Created by chenxiaokang on 2016/12/1. */public class MyViewB extends View{    private int mRectCount = 30;    private int width;    private int height;    private int currentHeight;    private int offset = 3;    private int mRectWidth = 20;    private int mStartPoint;    private Paint paint;    public MyViewB(Context context) {        this(context, null);    }    public MyViewB(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public MyViewB(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics metrics = new DisplayMetrics();        wm.getDefaultDisplay().getRealMetrics(metrics);        width = metrics.widthPixels;        height = (int) (metrics.heightPixels*1.0f*4/5);        currentHeight = (int) (height*1.0f*3/5);        mStartPoint = (width - mRectCount*mRectWidth)/2;        paint = new Paint();        paint.setStyle(Paint.Style.FILL);        paint.setAntiAlias(true);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(width, height);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        LinearGradient linearGradient = new LinearGradient(0, 0, width, height, Color.RED, Color.GREEN, Shader.TileMode.CLAMP);        paint.setShader(linearGradient);    }    @Override    protected void onDraw(Canvas canvas) {        for(int i = 0; i<mRectCount; i++){            currentHeight = (int) (Math.random()*height);            canvas.drawRect(mStartPoint+offset+mRectWidth*i, height-currentHeight,                            mStartPoint+mRectWidth*(i+1), height, paint);        }        postInvalidateDelayed(200);    }}

效果:

这里写图片描述

就是那种音频跳动的效果

8)自定义ViewGroup

package com.chen.demo.view;import android.content.Context;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.view.WindowManager;import android.widget.Scroller;/** * Created by chenxiaokang on 2016/12/2. */public class MyViewGroup extends ViewGroup{    int width, height;    Scroller mScroller;    public MyViewGroup(Context context) {        this(context, null);    }    public MyViewGroup(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);        DisplayMetrics metrics = new DisplayMetrics();        wm.getDefaultDisplay().getRealMetrics(metrics);        width = metrics.widthPixels;        height = metrics.heightPixels;        mScroller = new Scroller(context);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int count = getChildCount();        for (int i = 0; i<count; i++){            View view = getChildAt(i);            measureChild(view, width, height);        }    }    //放置每个子view的位置    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int childCount = getChildCount();        MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();        mlp.height = height * childCount;        setLayoutParams(mlp);        for(int i = 0; i<childCount; i++){            View view = getChildAt(i);            if(view.getVisibility() != View.GONE){                view.layout(l, i*height, r, (i+1)*height);            }        }    }    int mLastY;    int mStart;    int mEnd;    @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()-height){                    dy = 0;                }                scrollBy(0, dy);                mLastY = y;                break;            case MotionEvent.ACTION_UP:                mEnd = getScrollY();                int dScrollY = mEnd - mStart;                if(dScrollY > 0){                    if(dScrollY < height/3){                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);                    }else {                        mScroller.startScroll(0, getScrollY(), 0, height - dScrollY);                    }                }else {                    if(-dScrollY < height/3){                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);                    }else {                        mScroller.startScroll(0, getScrollY(), 0, -height-dScrollY);                    }                }                break;        }        postInvalidate();        return true;    }    @Override    public void computeScroll() {        super.computeScroll();        if(mScroller.computeScrollOffset()){            scrollTo(0, mScroller.getCurrY());            postInvalidate();        }    }}

效果:

这里写图片描述

9)总结

我们可以看见,自定义View并不是很难,只要安装基本流程一步步来,终将可以写出nb的控件,再厉害的控件都是由最基本的组件组成的,只要熟练掌握好这些控件的使用方法,难得也就不再难了。

10)Ref

1)《Android群英传》
2) http://www.cnblogs.com/cowboybusy/archive/2012/08/26/2718888.html
3)http://blog.csdn.net/guolin_blog/article/details/17357967

0 0
原创粉丝点击