I’ve tried to follow the Google Android tutorials and documentation when it comes to creating your own custom views but… it’s lacking. Thankfully the internet is full of information. Here’s a few resources to use and look at that helped me understand all that was needed to create my own custom view class from the ground up. They are not complete and taken together are still not complete which is why I felt the need to write this post:

  1. Google’s Android page on custom views.
  2. Google’s api demo about custom views.
  3. Google’s attr.xml from the api demo.
  4. Google’s custom_view_1.xml from the api demo.
  5. Pocket Journey’s article about custom attribute tags.
  6. Kevin Dion’s article about custom XML attribute tags in Widgets.
  7. An SO question with a fantastic answer about custom attribute tags.

The important things are the onDraw function, onMeasure function, and custom XML attributes.

Customizing the onDraw Method

The actual implementation of the onDraw method of a View is purposefully left up to the implementor. You can see just how determined they were to force the hand of developers by looking at the code on the git repository the Android developer’s left us:

protected void onDraw(Canvas canvas){}

It’s about as empty as you can get. There is no default functionality provided.

From a conceptual standpoint, at least for me, the easiest implementation of what an onDraw method should look like has got to be found within the ImageView class definition. ImageView, found in the widgets section of the Android code, inherits straight from View and thus we needn’t be concerned with any super class functionality clouding the logic of the member function. But, as any good engineer does when thinking about how the code might change in the years ahead, there is a call back to the onDraw of the base View class:

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    if (mDrawable == null) {        return; // couldn't resolve the URI    }    if (mDrawableWidth == 0 || mDrawableHeight == 0) {        return;     // nothing to draw (empty bounds)    }    if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {        mDrawable.draw(canvas);    } else {        int saveCount = canvas.getSaveCount();        canvas.save();        if (mCropToPadding) {            final int scrollX = mScrollX;            final int scrollY = mScrollY;            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,                            scrollX + mRight - mLeft - mPaddingRight,                            scrollY + mBottom - mTop - mPaddingBottom);        }        canvas.translate(mPaddingLeft, mPaddingTop);        if (mDrawMatrix != null) {            canvas.concat(mDrawMatrix);        }        mDrawable.draw(canvas);        canvas.restoreToCount(saveCount);    }}

Less complicated than that is the TextView implementation of the onDraw method, again found in the git repository. It’s much too large of a code snippet to replicate here, you’ll have to follow the link yourself. The reason it is so large is that it handles the drawing of not one but nine different Drawable objects plus layout logic and functionality for movable types. There’s also a lot more work going on and an additional inner class with it’s own onDraw method defined. That one is small enough where I could link it:

@Overridepublic void onDraw(Canvas c) {    mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);    if(mPositionOnTop) {        c.save();        c.rotate(180, (mRight - mLeft) / 2, (mBottom - mTop) / 2);        mDrawable.draw(c);        c.restore();    } else {        mDrawable.draw(c);    }}

In short, it appears that the onDraw methods concern themselves with drawing drawables and making sure that they are drawn in the correct positions relative to one another. Hence, a custom onDraw method should concern itself with the same and attempt only to respond to stateful changes within the class as they affect drawables.

And if you don’t have any drawables? The Android guide encourages us to implement our own onDraw method but what they leave out is that it’s not required. Just try to follow down the class hierarchy of the ListView on the git repository: ListView, AbsListView, AdapterView, ViewGroup, and back to View. Not a single one implements the onDraw routine. They really don’t need to.

In every Android class that I read from the repository I noticed one more thing: not a single one of them touched the draw member function. For good reasons too. Here’s the warning from View:

/** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is * called. When implementing a view, do not override this method; instead, * you should implement {@link #onDraw}. * * @param canvas The Canvas to which the View is rendered. */

If you’re thinking about creating your own custom view, I suggest heading the warning.

Customizing the onMeasure Method

Unlike onDraw above the View has a default implementation of the onMeasure function. It sets the default size to the background size. If you read the guide to creating custom views you’ll see that it claims it will set the size to a default of 100×100 but if you read the documentation or look at the code on the git repository:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

you won’t find a default 100 value. In fact, I didn’t see any default value (correct me if I’m wrong or overlooked something, please.)

Personally I find the example Android left for developers a bit lacking. I was expecting at least some sort of guidance for writing an onMeasure function, not a verbatim copy of the code in the standard View. Basically it left a lot of questions unanswered: What sorts of things should I be concerned with? What objects or view properties should affect the size of the view and more importantly how? Clearly a background image could be truncated by the size of the View but how to implement that properly?

All these things and more were answered by exploring the source code for Android itself. The code for some or all of the standard Android Views should have been hyper-linked. None of the different Views contained the answers to every question but together, they got me thinking in the right directions. I suggest anyone who is seriously considering writing their own custom View to first ask if they really need a custom view and then go straight to the source code for answers.

Looking at the git respository both ImageView and TextView have large onMeasure methods defined within their class definitions. Large enough that their size would detract from this simple blog article. ListView, on the other hand, has a rather straight-forwardish implementation:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    // Sets up mListPadding    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int widthMode = MeasureSpec.getMode(widthMeasureSpec);    int heightMode = MeasureSpec.getMode(heightMeasureSpec);    int widthSize = MeasureSpec.getSize(widthMeasureSpec);    int heightSize = MeasureSpec.getSize(heightMeasureSpec);    int childWidth = 0;    int childHeight = 0;    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();    if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||            heightMode == MeasureSpec.UNSPECIFIED)) {        final View child = obtainView(0, mIsScrap);        measureScrapChild(child, 0, widthMeasureSpec);        childWidth = child.getMeasuredWidth();        childHeight = child.getMeasuredHeight();        if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(                ((LayoutParams) child.getLayoutParams()).viewType)) {            mRecycler.addScrapView(child);        }    }    if (widthMode == MeasureSpec.UNSPECIFIED) {        widthSize = mListPadding.left + mListPadding.right + childWidth +                    getVerticalScrollbarWidth();    }    if (heightMode == MeasureSpec.UNSPECIFIED) {        heightSize = mListPadding.top + mListPadding.bottom + childHeight +                     getVerticalFadingEdgeLength() * 2;    }    if (heightMode == MeasureSpec.AT_MOST) {        // TODO: after first layout we should maybe start at the first visible position, not 0        heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);    }    setMeasuredDimension(widthSize, heightSize);    mWidthMeasureSpec = widthMeasureSpec;}

You can see how the code focuses not only on adjusting the size to fit the view but also to fit the contained children Views, a scroll bar, and a fading edge. It gives a much better feel for the types of things that need consideration. Looking at it it’s clear that attempting to add fancy graphics like fading edges and such to a view is going to have impacts outside of the onDraw or rendering code segments.

This is important information. This is what I wanted. This is something I wish they would have left in their guide. Most people aren’t like me; they aren’t going to dip into code that is open-sourced to learn the why and how of things until they absolutely have no choice. Then again, given what’s up on the guide, there really isn’t much of a choice at all.

One last thing, it’s important to note in the code snippet above the second to last line in the function. A call to the setMeasuredDimension function is required of all onMeasure implementations as per the guidelines and the onMeasure documentation. Without it the View documentation states that the measure function will throw an IllegalStateException.

Defining Custom XML Attributes

Most of the articles that I’ve read contain a stock sample of how to create a custom attributes xml file. I’ll follow their lead and provide a sample of custom XML Attributes for a mythical custom View called “TiledView”:

<?xml version="1.0" encoding="utf-8"?><resources>     <declare-styleable name="TiledView">          <attr name="tilingProperty" format="integer" min="0"/>         <attr name="tilingResource" format="color|reference"/>         <attr name="tileName" format="string" localization="suggested"/>          <attr name="tilingMode">             <flag name="center" value="0"/>             <flag name="stretched" value="1"/>             <flag name="repeating" value="3"/>         </attr>         <attr name="tilingEnum">              <enum name="under" value="0" />             <enum name="over" value="0" />         </attr>     </declare-styleable></resources>

The custom attributes need to be placed within a named “declare-styleable” tag so that the Android build system recognizes what they represent. The name of the tag relates directly to the name of the custom View.

Taking this example to the next step, some of the custom XML attributes could then be used in a layout XML of a project like so:

<?xml version="1.0" encoding="utf-8"?><com.owein.TiledView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:owein="http://schemas.android.com/apk/res/com.owein" android:orientation="vertical" owein:tilingProperty="5" owein:tilingMode="center"/>

One interesting thing here is that I’ve made use of an XML namespace to access my custom attributes. Android attributes are the same way, the “android:” preffix is used to work with them. While there is no requirement to actually place them within a namespace, if Android ever decides to add an attribute to all views or whichever View my custom View inherits which had the same name as my own custom attribute I’m protected (just like you use namespaces in standard programming practices.)

Custom Attribute Types

Some attributes take resources defined elsewhere such as drawables, some take numbers, some dimensions, and then there are preset values such as “fill_parent.” In the above list I’ve included the a number of different types of custom XML attributes. All are used in an identical manner within the layout so the choice of which type to make depends on the intended usage.

All styles of custom XML attributes are specified in a like manner: they are designated by the “attr” tag and contain a “name” attribute. The “attr” specifier exists to let the Android compiler know that we are declaring a new custom attribute type. The “name” attribute is actually the namespace prefixed attribute you will use when specifying it’s value in the layout XML.

From the example above you can see that I have a custom attribute named “tilingProperty” and in the layout I’ve got an attribute of the same name. That is how Android knows how to link these together. It also contains two other accompanying attributes: a “format” attribute and a “min” attribute. Before we get into what each format type can do let’s talk about the various types of formats.

Custom XML Attribute Formats

The “format” tag tells Android what sorts of values it should expect. The Android compiler is smart enough to take those value types and check them against the layout XML. That’s how the Android compiler creates a compile warning, alerting developers using your custom View to an issue just like when they use a built-in View. If there are multiple format types (such as colors and references to drawables) that should be allowed they are designated by the pipe “|” symbol. Optionally, you can create an attribute tag without a format tag using enums or flags. See below.

I don’t believe any of the linked blogs/posts/articles/Android develop guide I’ve mentioned in the intro list the predefined formats because there is no list of these things anywhere. The best that I can do is give an incomplete list based on the information found within the git repository. Perusing the attrs.xml file:

  • integer – take either standard numericals or hexadecimal enumerations
  • float – decimal notation
  • fraction – a percentage, x% or x%p (relative to parent), where x is an integer.
  • boolean – a True or False value
  • reference – a reference to a resource, either a built-in or one found in an XML file
  • color – one of the colors found within the standard color wheel
  • dimension – can be of any of the dimension types specified in the Android developer guide
  • string – a string or a string specified in one of the resource string files

If there are others I could not find them. On the other hand, I can’t see how there could be other types. It’s a fairly comprehensive list. What else could they add other than a location type or tuple type?

Custom XML Attribute Flags

Flags are special attribute types in that they are allowed only a very small subset of values, namely those that are defined underneath the attribute tag. Flags are specified by a “name” attribute and a “value” attribute. The names are required to be unique within that attribute type but the values need not be. This is the reason that during the evolution of the Android platform we had “fill_parent” and “match_parent” both mapping to the same behavior. Their values were identical.

The name attribute maps to the name used in the value place within the layout XML and does not require a namespace prefix. Hence, for the “tilingMode” above I chose “center” as the attribute value. I could have just as easily chosen “stretched” or “repeating” but nothing else. Not even substituting in the actual values would have been allowed.

The value attribute must be an integer. The choice of hexadecimal or standard numeral representation is up to you. There’s a few places within the Android code where both are used and the Android compiler is happy to accept either.

Custom XML Attribute Enums

Enums are used in an almost identical manner as flags with one provision, they may be used interchangeably with integers. Under the hood Enums and Integers are mapped to the same data type, namely, an Integer. When appearing in the attribute definition with Integers, Enums serve to prevent “magic numbers” which are always bad. This is why you can have an “android:layout_width” with either a dimension, integer, or named string “fill_parent.”

To put this into context, let’s suppose that I create a custom attribute called “layout_scroll_height” which accepts either an integer or a string “fill_parent.” To do so I’d add an “integer” format attribute and follow that with the enum:

<attr name="layout_scroll_height" format="integer"> <enum name="scroll_to_top" value="-1"/> </attr>

The one stipulation when using Enums in this manner is that a developer using your custom View could purposefully place the value “-1″ into the layout parameters. This would trigger the special case logic of “scroll_to_top.” Such unexpected (or expected) behavior could quickly relegate your library to the “legacy code” pile if the Enum values were chosen poorly.

Custom XML Integers

Integers need no explanation. They map directly to the Java Integer, a 32 bit signed value between -4294967295 to 4294967295 (and if you need more maybe Android isn’t the right place for your application.) While I’ve already stated that they may be freely mixed with Enums they may also have an optional “min” attribute. As it’s name might imply, using it declares to the Android compiler that all Integers of that attribute must have equal to or greater value to the “min” value. Should I have tried to put in “-1″ into my “owein:tilingProperty” within the layout XML file above, the compiler would have thrown a hissy-fit. I defined the minimum for that value as zero.

The “min” attribute can only be used with Integer only values. That is, they may not be used with mixed type values; those containing a pipe “|” such as “integer|resource.” Think about why for a second. How should a resource translate to a minimum value? It can’t. Hence the restriction.

Custom XML Strings

Strings also should need no explanation. They map directly to the Java String. I am mentioning them here because they have an optional attribute called “localization” which can be defined within the attribute definition (I just read that and it’s confusing to me too, don’t worry.) Up above I declared a “tilingName” custom attribute for my mythical custom View. In all honesty I have not seen any other setting than “suggested” for this optional tag so I have to question what it does other than allow you to use the built-in localization support in Android.

Conclusion

Custom Views and custom XML attributes take some experimentation to get used to. As is the nature with Android phones, something that looks good and performs well on one phone is going to lead to trouble on another phone. The onMeasure and onDraw methods are more art than science (at least at this point in my Android experimentation career.) I give my hats off to anyone who first finds a valid reason to need a custom View and then makes it work on as many platforms as possible.

Let me just say that this is a monster post that has taken me a couple weeks to put together. At first I tried dividing it up into two different posts but then I reasoned that without one the other would not make sense so I put it back together. Then I broke them up. Now they’re back together. If there’s anything wrong or false within it, please correct me. I’d happily update it and give you credit. I also feel like I’m missing something important but in the spirit of “just getting it out there” I’m going to post it.

I hope this helps anyone reading it and if you want, feel free to share it with anyone (then pass their corrections/comments back to me.) Peer review, it exists to make us all better.

更多相关文章

  1. 代码中设置drawableleft
  2. android 3.0 隐藏 系统标题栏
  3. Android开发中activity切换动画的实现
  4. Android(安卓)学习 笔记_05. 文件下载
  5. Android中直播视频技术探究之—摄像头Camera视频源数据采集解析
  6. 技术博客汇总
  7. android 2.3 wifi (一)
  8. AndRoid Notification的清空和修改
  9. Android中的Chronometer

随机推荐

  1. Android获取语言及地区总结
  2. Android侧滑栏无法滑动收起的解决
  3. Android夜间模式官方api实现(AppCompatDe
  4. 《Android开发从零开始》——1.Android开
  5. Android系统中设置TextView的行间距(非行
  6. Android(安卓)JNI实战
  7. 详解 Android(安卓)的 Activity 组件
  8. Android的硬件加速
  9. Android(安卓)源码查看
  10. android:launchMode="singleTask" 与 onN