自定义View:3、编写自己的ViewGroup

前面我们已经分析过两种自定义View的方法:

今天我们来继续学习第三种自定义View的方法,继承ViewGroup,实现自己的Layout。什么意思呢?其实前两种自定义View的方法,我们都是编写的具体的每一个View,然后整合到我们现有的Layout(比如LinearLayout,RelativeLayout等等)当中,但很多情况下,自定义View并不能完全满足我们的需求,或者说,我们想要使用现成的控件,但我们希望我们的界面上,我们可以完全自己来控制如何摆放这些控件。那这个时候怎么做呢?

我们先来看一下我们想要实现的效果是什么样子的?

device-2014-12-13-190320

简单分解一下上面我们要实现的需求:

  • 每一小块为自定义View(称之为磁贴),大小共有3中规格,分别是:横向占据屏幕1/4(size=one),横向占据屏幕1/2(size=two),横向占据屏幕全部(size=four)。其高度始终为屏幕1/4。颜色与文字均可以通过XML直接指定。
  • 要求在XML定义各个View之后,要求能够从左上角开始能够自动占据铺满屏幕。

好了,要求说完了,那么该怎么去实现呢?首先我们来分解任务,要实现上面的功能,其实我们总共需要两个步骤,第一步,参考自定义View:1、定制自己的饼形图,定制我们自己的磁贴。第二步,指定我们自定的ViewGroup,自动对添加的磁贴贴到合适的位置上。

一、定制磁贴

1、定制属性

首先,我们需要在attrs.xml中为磁贴定义一些属性,在这边定义之后,我们就可以直接在Layout的XML中为磁贴指定颜色,文字,大小等内容。代码如下:

    <declare-styleable name="Tile">
        <attr name="background" format="color"/>
        <attr name="title" format="string"/>
        <attr name="size" format="enum">
            <enum name="one" value="1"/>
            <enum name="two" value="2"/>
            <enum name="four" value="4"/>
        </attr>
    </declare-styleable>

如上所示,我们总共为磁贴定义了3个属性,其中包括背景颜色,文字内容,以及尺寸,one代表占据宽度的1/4,two代表占据宽度的1/2,而four则代表了占据宽度的全部。

2、定义View:Tile

接下来就是很重要的一步,定义View,即Tile,还记得我们之前的文章具体是怎么做的吗?没做,就是分3步走:

  1. 构造函数从XML中获取参数,初始化。
  2. 覆写onMeasure方法,指定View具体的大小。
  3. 覆写onDraw方法,按照参数指定的颜色,文字及大小进行绘制。

初始化

   public Tile(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Tile, 0, 0);
        mContent = new TileContent();
        try {
            mContent.mBackgroundColor = typedArray.getColor(R.styleable.Tile_background, Color.BLUE);
            mContent.mTitle = typedArray.getString(R.styleable.Tile_title);
            mContent.mSize = typedArray.getInt(R.styleable.Tile_size, 1);
        } finally {
            typedArray.recycle();;
        }

        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRect = new Rect();
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(50.0f);
        mTextSize = new Rect();
        mTextPaint.getTextBounds(mContent.mTitle, 0, mContent.mTitle.length(), mTextSize);
    }

初始化代码比较简单了:

  1. 从context.getTheme()/obtainStyeAttributes中获取包含属性的TypeArray。
  2. 从中读取设置的参数
  3. 初始化画笔,获取文字所占的边界大小。

onMeasure(init widthMeasureSpec, int heightMeasureSpec)

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;

        //如果指定了尺寸,那么就使用指定的尺寸,否则使用我们容器尺寸的一半
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
            height = width;
        } else {
            switch (mContent.mSize) {
                case SIZE_ONE :
                    width = widthSize >> 2;
                    height = width;
                    break;
                case SIZE_TWO:
                    width = widthSize >> 1;
                    height = width >> 1;
                    break;
                case SIZE_FOUR:
                    width = widthSize;
                    height = width >> 2;
                    break;
            }
        }
        mRect.set(0, 0, width, height);
        setMeasuredDimension(width, height);
    }

在这个界面设置当中,onMeasure方法是比较关键的一步,因为我们要在这个地方为每一个磁贴,根据他们配置的属性为其指定合适的尺寸,那么我们具体是怎么做的呢?

  1. 获取widthMode,如果是MeasureSpec.EACTLY,即在XML中指定了具体的大小,那么我们就应该使用指定的大小。
  2. 如果没有指定具体的大小,而是让View根据需求来自己指定的话,我们就按照原先的设计,如果尺寸为one,那么就将宽度设置为全部的宽度1/4,高度则与宽度想的呢个,如果尺寸为two,那么则宽度为全部宽度的一半,高度为对应宽度的1/2,如果为four,则宽度为全部的宽度,高度是宽度的1/4。

onDraw

onDraw方法是我们经常用到的方法了,简单的讲就是根据我们的需求画图呗,看看代码就行。

   @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(mContent.mBackgroundColor);
        canvas.drawRect(mRect, mPaint);
        mTextPaint.setColor(Color.WHITE);
        canvas.drawText(mContent.mTitle, getMeasuredWidth()/2 - mTextSize.width()/2, getMeasuredHeight()/2 + mTextSize.height()/2, mTextPaint);
    }

上面的代码中,我们首先画好北京颜色,然后设置文字的画笔颜色,为其在中间写好文字。

好了,至此,具体的Tile就准备完毕了,再来看怎么编写我们自定的ViewGroup

二、定制ViewGroup:TileLayout

1、覆写onMeasure方法

自定义View的onMeasure方法我们有写过,但是自定义Layout的onMeasure方法怎么写呢?因为ViewGroup继承自View,因此思路和View基本上差别不大,我们这里先简单考虑,我们先将整个屏幕的全部空间都占据使用。那该怎么写呢?

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

其中做了两个工作:

  • 依次通知子View们进行测量
  • 调用父类的onMeasure方法。

 

2、onLayout布局

该方法是ViewGroup的核心方法之一,简单的理解,在该方法中,我们需要设计将子元素放在合适的位置上。根据需求,我们对TileLayout的onLayout方法可以如下实现:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.i(TAG, "onLayout:" + " position:" + left + "," + top + "," + right + "," + bottom);

        int positionX = left;
        int positionY = top;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if(! (childView instanceof Tile) ) {
                throw new IllegalArgumentException("Catch Exception not Tile!");
            }
            Tile item = (Tile) childView;

            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();

            int itemPostitionX = left;
            int itemPositionY = top;
            switch (item.getSize()) {
                case Tile.SIZE_ONE:

                    if( !isSpaceEmpty(Tile.SIZE_ONE) ) {
                        LayoutPositions params = mSizeOneSpaces.remove();
                        itemPostitionX = params.x;
                        itemPositionY = params.y;
                    } else {
                        if (positionX + width <= right) {
                            itemPostitionX = positionX;
                            itemPositionY = positionY;
                            positionX += width;
                            if (positionX >= right) {
                                positionX = left;
                                positionY = positionY + height;
                            }
                        } else {
                            itemPostitionX = left;
                            itemPostitionX = positionY + height;
                            positionX = itemPostitionX + width;
                            positionY = itemPositionY;
                        }
                    }
                    break;
                case Tile.SIZE_TWO:
                    if (positionX + width <= right) {
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                        positionX += width;
                        if (positionX >= right) {
                            positionX = left;
                            positionY = positionY + height;
                        }
                    } else {
                        for (int start=positionX; start + (width >> 1) <= right; start=start + (width >> 1)) {
                            mSizeOneSpaces.add(new LayoutPositions(start, positionY));
                        }
                        positionX = left;
                        positionY = positionY + height;
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                    }
                    break;
                case Tile.SIZE_FOUR:
                    if (positionX == left) {
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                        positionX += width;
                        positionY += height;
                    } else {
                        for (int start=positionX; start + (width >> 2) <= right; start=start + (width >> 2)) {
                            mSizeOneSpaces.add(new LayoutPositions(start, positionY));
                        }
                        positionX = left;
                        positionY = positionY + height;
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                        positionX = left;
                        positionY = positionY + height;
                    }
                    break;
            }
            Log.i(TAG, "Item:" + item.getTitle() + " position:" + itemPostitionX + "," + itemPositionY + "," + (itemPostitionX + width) + "," + (itemPositionY + height));
            childView.layout(itemPostitionX, itemPositionY, itemPostitionX + width, itemPositionY + height);
        }
    }

其实思路也很简单,首先尝试摆放,如果能将子View摆在某处,那么则摆放这里,继续摆放下面的地方,如果不能,则说明此行空间不足,那么将剩下的空间根据大小分配给合适的数量的size=“one”的元素。依次摆放就可以。

三、使用

按照我们设计的,将所有的属性设置都放在XML当中,Activity代码如下:

public class TestTileLayout extends Activity{
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tile);
    }
}

布局代码:

<?xml version="1.0" encoding="utf-8"?>
<me.happyhls.androiddemo.view.TileLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tile="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#0000FF"
        tile:title="Title1"
        tile:size="one"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#00FF00"
        tile:title="Title2"
        tile:size="two"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#FF0000"
        tile:title="Title3"
        tile:size="four"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#0000FF"
        tile:title="Title4"
        tile:size="one"
        />

    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#00FF00"
        tile:title="Title5"
        tile:size="one"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#FF00FF"
        tile:title="Title6"
        tile:size="two"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#FF000F"
        tile:title="Title7"
        tile:size="four"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#0000FF"
        tile:title="Title8"
        tile:size="one"
        />
</me.happyhls.androiddemo.view.TileLayout>

好了,上面就是简单的对自定义ViewGroup的使用。总结来说,我们只是大体了解了一下如何自定义View,如何自定义ViewGroup,但深入的我们依然没有设计到,因此后面会有至少两篇文章,我们来分析Android原生控件TextView和Android原生Layout:LinearLayout的源代码。

About: happyhls


发表评论

电子邮件地址不会被公开。 必填项已用*标注