Android群英传:控件架构与自定义控件

1. Android控件架构

Android群英传:控件架构与自定义控件_第1张图片
Android控件树
Android群英传:控件架构与自定义控件_第2张图片
UI界面架构

Android中每一个Activity都包含一个Window对象。
Window对象通常由PhoneWindow。
PhoneWindow将一个 DecorView作为整个应用窗口的根View。
DecorView将要显示的内容呈现在PhoneWindow上。在显示上,它将屏幕分成两部分:TitleView和ContentView。
ContentView是一个id为content的FrameLayout,其中activity_main.xml就是设置到这个FrameLayout中的。


Android群英传:控件架构与自定义控件_第3张图片
Android标准视图树

如图所示的第二层封装了一个LinerLayout作为ViewGroup,如果用户通过requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏显示,视图树中的布局就只有content了,这也就解释了为什么调用requestWindowFeature()方法一定要在setContentView()方法之前调用。

在代码中,当程序在onCreate()方法中调用了setContentView()方法之后,AMS会调用onResume()方法,此时系统 才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制。

2. View的测量和绘制

View 的测量

现实生活中,绘制一个图形,我们必须要先知道绘制的大小和位置,这同样也适用于Android。

在Android中,绘制View之前,必须对View做测量,告知系统需要绘制的View的大小。这一过程在onMeasure()方法中来完成。
MeasureSpec类:用于View的测量。它是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小。

EXACTLY:精确模式。当控件的大小是具体的值或者指定为match_parent时,系统使用的是EXACTLY模式。
AT_MOST:最大值模式。当控件的宽高属性指定为wrap_content时,控件的大小一般会随着控件内容或者子控件的变化而变化。此时控件的尺寸只要不超过父控件的大小即可。
UNSPECIFIED:不指定测量大小的模式。View想多大就多大,通常情况下,在绘制自定义View时才会使用。

View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式。此时,控件可以相应你指定的具体宽高值或者match_parent属性。而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法来制定wrap_content时的大小。

View 的绘制

测量好一个View之后,就可以简单的通过重写onDraw()方法,在Canvas对象上绘制出所需要的图形。

绘制View的关键:Paint、Canvas

Canvas对象的创建需要传入一个bitmap对象,这个过程叫过装载画布,传入的bitmap对象用来存储所有绘制在Canvas上的像素信息。Canvas canvas = new Canvas(bitmap)。当用这种方式创建了Canvas对象之后,后面调用所有的Canvas.drawXXX方法都将发生在这个bitmap对象上。

       /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

onDraw()方法的调用时机是在步骤3中,即绘制view的content。

3. ViewGroup的测量和绘制

ViewGroup具有管理其子View的职责。

ViewGroup的测量

如果ViewGroup的大小为wrap_content,就需要遍历所有的子View,以便获得所有子View的大小,从而来决定自己的大小。
其他模式下,ViewGroup会通过具体的指定值来设置自身大小。

ViewGroup的绘制

ViewGroup 通常情况下不需要绘制,因为其本身没有需要绘制的东西,但是ViewGroup会使用dispatchDraw()方法来绘制子View,过程是遍历所有子View并调用子View的绘制方法来完成工作。

4. 自定义控件的三种方式

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

在View中通常有以下一些比较重要的回调方法:

  • onFinishInflate():从XML加载组件后回调
  • onSizeChanged():组件大小改变时回调
  • onMeasure():回调该方法来进行测量
  • onLayout():回调该方法来确定显示的位置
  • onTouchEvent():监听到触摸事件时回调

下面对自定义控件实现的三种情况逐个展开来描述。

对现有控件的扩展

这是一个很重要的自定义View的方法,它可以在原生控件的基础上进行拓展,增加新的功能、修改显示的UI等。通常我们可以在onDraw()方法中对原生控件进行扩展。

    @Override
    protected void onDraw(Canvas canvas) {
        // 在回调父类方法前,实现自己的逻辑
        super.onDraw(canvas);
        // 在回调父类方法后,实现自己的逻辑
    }

比如说我们自定义一个TextView。在回调方法前或者回调方法后实现自己的逻辑,会有不同的效果。


在回调方法前实现绘制背景边框的逻辑

在回调方法后实现绘制背景边框的逻辑

可见,Android的绘制时一层层叠加的,有点类似于Photoshop中的图层。

通过组合来实现新的控件

创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常需要一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。
通过组合来实现新的控件通常会包含以下几点内容:

  • 定义属性
  • 组合控件
  • 暴露接口给调用者
  • 实现接口回调

下面,我们就以实现一个topbar为例,来看一下通过组合来实现新控件的思路。

  1. 定义属性
    在res/values/目录下创建一个attrs.xml的属性文件。
res/values/attrs.xml




    
        
        
        
        
        
        
        
        
        
    


declare-styleable标签用来声明使用自定义属性,attr用来声明具体的自定义属性。

我们可以通过下面的方式来获取XML布局文件中自定义的那些属性。

TypedArray ta = context.obtainStyledAttributes(attrs,  R.styleable.TopBar);

系统提供了TypeArray这样的数据结构来获取自定义属性集。

        // 从TypedArray中取出对应的值来为要设置的属性赋值
        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);
        mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
        mRightText = ta.getString(R.styleable.TopBar_rightText);

        mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
        mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
        mTitle = ta.getString(R.styleable.TopBar_title);

        // 获取完TypedArray的值后,一般要调用recyle方法来避免重新创建的时候的错误
        ta.recycle();

获取完所有的属性后,需要调用TypeArray的recycle()方法来完成资源的回收。

  1. 组合控件
    UI模板TopBar由三个控件组成,左边的点击按钮,右边的点击按钮和中间的的标题栏。通过动态添加控件,使用addView()方法将这三个控件添加到自定义的TopBar模板中,并给他们设置我们前面所获取到的具体属性值。比如,标题的颜色 、大小等。
        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);

        // 为创建的组件元素赋值
        // 值就来源于我们在引用的xml文件中给对应属性的赋值
        mLeftButton.setTextColor(mLeftTextColor);
        mLeftButton.setBackground(mLeftBackground);
        mLeftButton.setText(mLeftText);

        mRightButton.setTextColor(mRightTextColor);
        mRightButton.setBackground(mRightBackground);
        mRightButton.setText(mRightText);

        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleTextColor);
        mTitleView.setTextSize(mTitleTextSize);
        mTitleView.setGravity(Gravity.CENTER);

        // 为组件元素设置相应的布局元素
        mLeftParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
        // 添加到ViewGroup
        addView(mLeftButton, mLeftParams);

        mRightParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
        addView(mRightButton, mRightParams);

        mTitlepParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitlepParams);
  1. 暴露接口给调用者
      // 按钮的点击事件,不需要具体的实现,
        // 只需调用接口的方法,回调的时候,会有具体的实现
        mRightButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                mListener.rightClick();
            }
        });

        mLeftButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                mListener.leftClick();
            }
        });

    // 暴露一个方法给调用者来注册接口回调
    // 通过接口来获得回调者对接口方法的实现
    public void setOnTopbarClickListener(topbarClickListener mListener) {
        this.mListener = mListener;
    }

    // 接口对象,实现回调机制,在回调方法中
    // 通过映射的接口对象调用接口中的方法
    // 而不用去考虑如何实现,具体的实现由调用者去创建
    public interface topbarClickListener {
        // 左按钮点击事件
        void leftClick();
        // 右按钮点击事件
        void rightClick();
    }
  1. 实现接口回调

在自己的业务代码中可以实现基于接口的回调

        // 为topbar注册监听事件,传入定义的接口
        // 并以匿名类的方式实现接口内的方法
        mTopbar.setOnTopbarClickListener(
                new TopBar.topbarClickListener() {

                    @Override
                    public void rightClick() {
                        Toast.makeText(TopBarTest.this,
                                "right", Toast.LENGTH_SHORT)
                                .show();
                    }

                    @Override
                    public void leftClick() {
                        Toast.makeText(TopBarTest.this,
                                "left", Toast.LENGTH_SHORT)
                                .show();
                    }
                });

重写View来实现全新的控件

当安卓系统的原生控件无法满足我们的需求时,我们就需要完全创建一个新的自定义View来实现需要的功能。
创建一个自定义View的难点在于:绘制控件和实现交互
通常需要继承View类,并重写它的onMeasure() 、onDraw()等方法来实现绘制逻辑,同时重写onTouchEvent()等触控事件来实现交互逻辑。当然,也可以引入自定义属性来丰富自定义View的可定制性。

5. 自定义ViewGroup

ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。因此,自定义ViewGroup通常需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法来增加响应事件。

相关代码如下:

    @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
    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
    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;
            default:
                break;
        }
        invalidate();
        return true;
    }

实现滑动代码

    mScroller.startScroll(int startX, int startY, int dx, int dy);
    invalidate();

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            // 实现自己的滚动业务, 比如:scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            ......
            postInvalidate();
        }
    }

6. 事件分发机制

触摸事件:捕获触摸屏幕后产生的事件。当点击一个按钮时,通常会产生三个事件——按钮按下,这是事件一;如果不小心滑一点,这是事件二;当手抬起,这是事件三。

7. 事件拦截机制

你可能感兴趣的:(Android群英传:控件架构与自定义控件)