美文网首页
Android群英传:控件架构与自定义控件

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

作者: ADark0915 | 来源:发表于2018-02-28 15:05 被阅读12次

    1. Android控件架构

    Android控件树 UI界面架构

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


    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
    
    <?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标签用来声明使用自定义属性,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群英传:控件架构与自定义控件

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