Android自定义控件

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值