Enhancing Android UI with Custom Views 通过自定义view来让你的UI更屌!

翻译 2016年08月31日 09:41:20
Enhancing Android UI with Custom Views (通过自定义view来让你的UI更屌)

There are many great advantages to building your own UI components, such as the ability to have full control of how your content is displayed. But one of the best reasons to become an expert at custom view creation is the ability to flatten your view hierarchy.


One custom view can be designed to do the job of several nested framework widgets, and the fewer views you have in your hierarchy, the better your application will perform.


Custom View(自定义视图)

Our first example will be a simple widget that displays a pair of overlapping image logos, with a text element on the right and vertically centered. You might use a widget like this to represent the score of a sports matchup, for example.


When we build custom views, there are two primary functions we must take into consideration:


  • Measurement (测量方法)
  • Drawing (绘制方法)

Let's have a look at measurement first...


View Measurement(视图的测量)

Before a view hierarchy can be drawn, the first task of the Android framework will be a measurement pass. In this step, all the views in a hierarchy will be measured top-down; meaning measure starts at the root view and trickles through each child view.


Each view receives a call to onMeasure() when its parent requests that it update its measured size. It is the responsibility of each view to set its own size based on the constraints given by the parent, and store those measurements by calling setMeasuredDimension(). Forgetting to do this will result in an exception.


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //Get the width measurement
    int widthSize = View.resolveSize(getDesiredWidth(), widthMeasureSpec);

    //Get the height measurement
    int heightSize = View.resolveSize(getDesiredHeight(), heightMeasureSpec);

    //MUST call this to store the measurements
    setMeasuredDimension(widthSize, heightSize);

Each view is given two packed-int values in onMeasure(), each know as a MeasureSpec, that the view should inspect to determine how to set its size. A MeasureSpec is simply a size value with a mode flag encoded into its high-order bits.


There are three possible values for a spec's mode: UNSPECIFIEDAT_MOST, and EXACTLY.UNSPECIFIED tells the view to set its dimensions to any desired size. AT_MOST tells the view to set its dimensions to any size less than or equal to the given spec. EXACTLY tells the view to set its dimensions only to the size given.

译文:有三种可能的测量模式:UNSPECIFIED,AT_MOST和EXACTLY。UNSPECIFIED指的是view视图可以设置任意大小的尺寸。AT_MOST指的是view视图可以设置成小于或等于给定的规范大小,EXACTLY 指的是视图只能设置为规定的尺寸大小。

The video tutorial mentions a MeasureUtils helper class to assist in resolving the appropriate view size. This tutorial has since replaced that utility with the built-inView.resolveSize() method to accomplish the same end.

It may also be important to provide measurements of what your desired size is, for situations where wrap_content will be used to lay out the view. Here is the method we use to compute the desired width for our custom view example. We obtain width values for the three major elements in this view, and return the space that will be required to draw the overlapping logos and text.


private int getDesiredWidth() {
    int leftWidth;
    if (mLeftDrawable == null) {
        leftWidth = 0;
    } else {
        leftWidth = mLeftDrawable.getIntrinsicWidth();

    int rightWidth;
    if (mRightDrawable == null) {
        rightWidth = 0;
    } else {
        rightWidth = mRightDrawable.getIntrinsicWidth();

    int textWidth;
    if (mTextLayout == null) {
        textWidth = 0;
    } else {
        textWidth = mTextLayout.getWidth();

    return (int)(leftWidth * 0.67f)
            + (int)(rightWidth * 0.67f)
            + mSpacing
            + textWidth;

Similarly, here is the method our example uses to compute its desired height value. This is governed completely by the image content, so we don't need to pay attention to the text element when measuring in this direction.


TIP: Favor efficiency over flexibility! Don't spend time testing and overriding states you don't need. Unlike the framework widgets, your custom view only needs to suit your application's use case. Place your custom view inside of its final layout, inspect the values the framework gives you for MeasureSpecs, and THEN build the measuring code to handle those specific cases.


View Drawing (视图的绘制)

A custom view's other primary job is to draw its content. For this, you are given a blank Canvas via the onDraw() method. This Canvas is sized and positioned according to your measured view, so the origin matches up with the top-left of the view bounds. Canvas supports calls to draw shapes, colors, text, bitmaps, and more.


Many framework components such as Drawable images and text Layouts provide their own draw() methods to render their contents onto the Canvas directly; which we have taken advantage of in this example.


protected void onDraw(Canvas canvas) {
    if (mLeftDrawable != null) {

    if (mTextLayout != null) {
        canvas.translate(mTextOrigin.x, mTextOrigin.y);



    if (mRightDrawable != null) {

Custom Attributes (自定义属性)

You may find yourself wanting to provide attributes to your custom view from within the layout XML. We can accomplish this by declaring a style-able block in the project resources. This block must contain all the attributes we would like to read from the layout XML.


When possible, it is most efficient to reuse attributes already defined by the framework, as we have done here. We are utilizing existing text, and drawable attributes, to feed in the content sources and text styling information that the view should apply.


<?xml version="1.0" encoding="utf-8"?>
    <declare-styleable name="DoubleImageView">
        <attr name="android:drawableLeft" />
        <attr name="android:drawableRight" />
        <attr name="android:text" />
        <attr name="android:textSize" />
        <attr name="android:textColor" />
        <attr name="android:spacing" />



    android:text="5 - 5"


During view creation, we use the obtainStyledAttributes() method to extract the values of the attributes named in our style-able block. This method returns a TypedArray instance, which allows us to retrieve each attribute as the appropriate type; whether it be a Drawable, dimension, or color.


DON'T FORGET: TypedArrays are heavyweight objects that should be recycled immediately after all the attributes you need have been extracted.


public DoubleImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mTextOrigin = new Point(0, 0);

    TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.DoubleImageView, 0, defStyle);

    Drawable d = a.getDrawable(R.styleable.DoubleImageView_android_drawableLeft);
    if (d != null) {

    d = a.getDrawable(R.styleable.DoubleImageView_android_drawableRight);
    if (d != null) {

    int spacing = a.getDimensionPixelSize(
            R.styleable.DoubleImageView_android_spacing, 0);

    int color = a.getColor(R.styleable.DoubleImageView_android_textColor, 0);

    int rawSize = a.getDimensionPixelSize(
            R.styleable.DoubleImageView_android_textSize, 0);

    CharSequence text = a.getText(R.styleable.DoubleImageView_android_text);


Custom ViewGroup(自定义ViewGroup)

Now that we've seen how easy it is to build our own custom content into a view, what about building a custom layout manager? Widgets like LinearLayout and RelativeLayout have A LOT of code in them to manage child views, so this must be really hard, right?

译文:现在你看到了吧,搞一个自定义view是多么容易的一件事,那搞一个自定义的布局管理器又如何呢?类似LinearLayout和RelativeLayout 这样的控件它内部有很多的代码去管理各种子视图,看上去这一定是很难是吗?

Hopefully this next example will convince you that this is not the case. Here we are going to build aViewGroup that lays out all its child views with equal spacing in a 3x3 grid. This same effect could be accomplished by nesting LinearLayouts inside of LinearLayouts inside of LinearLayouts...creating a hierarchy many many levels deep. However, with just a little bit of effort we can drastically flatten that hierarchy into something much more performant.


ViewGroup Measurement (ViewGroup的测量)

Just as with views, ViewGroups are responsible for measuring themselves. For this example we are computing the size of the ViewGroup using the framework's getDefaultSize() method, which essentially returns the size provided by the MeasureSpec in all cases except when an exact size requirement is imposed by the parent.


ViewGroup has one more job during measurement, though; it must also tell all its child views to measure themselves. We want to have each view take up exactly 1/3 of both the containers height and width. This is done by constructing a new MeasureSpec with the computed fraction of the view size and the mode flag set to EXACTLY. This will notify each child view that they must be measured to exactly the size we are giving them.


One method of dispatching these commands it to call the measure() method of every child view, but there are also helper methods inside of ViewGroup to simplify this process. In our example here we are calling measureChildren(), which applies the same spec to every child view for us. Of course, we are still required to mark our own dimensions as well, viasetMeasuredDimension(), before we return.

译文:分发这些命令的一个方法就是分别去调用每个子视图的measure()方法,在ViewGroup中也有一个更好的方法可以简化这个过程。在我们的这个示例中,我们调用了ViewGroup 的 measureChildren()方法,它可以为我们将同样的尺寸规范应用到每个子视图身上。当然,在return之前,我们仍然需要通过调用setMeasuredDimension()方法来设置好自己的测量尺寸。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize, heightSize;

    //Get the width based on the measure specs
    widthSize = getDefaultSize(0, widthMeasureSpec);

    //Get the height based on measure specs
    heightSize = getDefaultSize(0, heightMeasureSpec);

    int majorDimension = Math.min(widthSize, heightSize);
    //Measure all child views
    int blockDimension = majorDimension / mColumnCount;
    int blockSpec = MeasureSpec.makeMeasureSpec(blockDimension,
    measureChildren(blockSpec, blockSpec);

    //MUST call this to save our own dimensions
    setMeasuredDimension(majorDimension, majorDimension);

Layout (布局)

After measurement, ViewGroups are also responsible for setting the BOUNDS of their child views via the onLayout() callback. With our fixed-size grid, this is pretty straightforward. We first determine, based on index, which row & column the view is in. We can then call layout() on the child view to set its left, right, top, and bottom position values.


protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int row, col, left, top;
    for (int i=0; i < getChildCount(); i++) {
        row = i / mColumnCount;
        col = i % mColumnCount;
        View child = getChildAt(i);
        left = col * child.getMeasuredWidth();
        top = row * child.getMeasuredHeight();

                left + child.getMeasuredWidth(),
                top + child.getMeasuredHeight());

Notice that inside layout we can use the getMeasuredWidth() and getMeasuredHeight()methods on the view. These will always be valid at this stage since the measurement pass comes before layout, and this is a handy way to set the bounding box of each child.


TIP: Measurement and layout can be as simple or complex as you make it. It is easy to get lost attempting to handle every possible configuration change that may affect how you lay out child views. Stick to writing code for the cases your application will actually encounter.


ViewGroup Drawing (ViewGroup的绘制)

While ViewGroups don't generally draw any content of their own, there are many situations where this can be useful. There are two helpful instances where we can ask ViewGroup to draw.


The first is inside of dispatchDraw() after super has been called. At this stage, child views have been drawn, and we have an opportunity to do additional drawing on top. In our example, we are leveraging this to draw the grid lines over our box views.


protected void dispatchDraw(Canvas canvas) {
    //Let the framework do its thing

    //Draw the grid lines
    for (int i=0; i <= getWidth(); i += (getWidth() / mColumnCount)) {
        canvas.drawLine(i, 0, i, getHeight(), mGridPaint);
    for (int i=0; i <= getHeight(); i += (getHeight() / mColumnCount)) {
        canvas.drawLine(0, i, getWidth(), i, mGridPaint);

The second is using the same onDraw() callback as we saw before with View. Anything we draw here will be drawn before the child views, and thus will show up underneath them. This can be helpful for drawing any type of dynamic backgrounds or selector states.


If you wish to put code in the onDraw() of a ViewGroup, you must also remember to enable drawing callbacks with setWillNotDraw(false). Otherwise your onDraw() method will never be triggered. This is because ViewGroups have self-drawing disabled by default.


More Custom Attributes (更多的自定义属性)

So back to attributes for a moment. What if the attributes we want to feed into the view don't already exist in the platform, and it would be awkward to try and reuse one for a different purpose?


In that case, we can define custom attributes inside of our style-able block. The only difference here is that we must also define the type of data that attribute represents; something we did not need to do for the framework since it already has them pre-defined.


Here, we are defining a dimension and color attribute to provide the styling for the box's grid lines via XML.


<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="BoxGridLayout">
        <attr name="separatorWidth" format="dimension" />
        <attr name="separatorColor" format="color" />
        <attr name="numColumns" format="integer" />

Now, we can apply these attributes externally in our layouts. Notice that attributes defined in our own application package require a separate namespace that points to our internal APK resources.


Notice also that our custom layout behaves no differently than the other layout widgets in the framework. We can simply add child views to it directly through the XML layout file.


<?xml version="1.0" encoding="utf-8"?>

Just for fun, we will even include the layout inside itself, to create the full 9x9 effect that you saw in the earlier screenshot. We have also defined a slightly thicker grid separator to distinguish the major blocks from the minor blocks.


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"


        <include layout="@layout/box_small" />

        <include layout="@layout/box_small" />

        <include layout="@layout/box_small" />

        <include layout="@layout/box_small" />




I hope that now you can see how simple it is to get started building custom views and layouts. Reduced dependence on the framework widgets leads to better user interfaces and less clutter in your view hierarchy. Your users and your devices will thank you for it.


Be sure to visit the GitHub link to find the full examples shown here, as well as others to help you get comfortable building custom views.


Thanks for your time today, and I hope you learned something new!




Android Studio中Git和GitHub使用详解(上篇)

一、Git和GitHub简述 1.Git 分布式版本控制系统,最先使用于Linux社区,是一个开源免费的版本控制系统,功能类似于SVN和CVS。Git与其他版本管理工具最大的区别点和优点就是分布式...

【Android Training UI】创建自定义Views(Lesson 1 - 创建一个View类)

发布在我的网站 http://kesenhoo.github.io/blog/2013/06/30/android-training-ui-creating-custom-views-lesson-1...

Android UI设计之<十二>自定义View,实现绚丽的字体大小控制控件FontSliderBar


android UI——自定义view(layout)重写方法的分析

android 开发中难免会遇到自定义view,或者自定义的layout。那么自定义的view或者layout应该注意哪些东西呢?...

Android 如何在自定义界面上启用输入法 (How to enable inputmethod for the custom UI)

http://www.oschina.net/question/54100_39046 在android中经常会自定义组件,自定义的组件可以通过继承系统的已经有的组件来实现。也可以直接继...
  • cstarbl
  • cstarbl
  • 2012年02月25日 17:18
  • 1296

Android UI编程自定义控件ImageButton

  • 2015年01月29日 21:04
  • 764KB
  • 下载
您举报文章:Enhancing Android UI with Custom Views 通过自定义view来让你的UI更屌!