Android Custom Views and XML attributes
Posted 小马识图
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Custom Views and XML attributes相关的知识,希望对你有一定的参考价值。
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:
- Google’s Android page on custom views.
- Google’s api demo about custom views.
- Google’s attr.xml from the api demo.
- Google’s custom_view_1.xml from the api demo.
- Pocket Journey’s article about custom attribute tags.
- Kevin Dion’s article about custom XML attribute tags in Widgets.
- 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:
@Override
protected 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:
@Override
public 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:
@Override
protected 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.
以上是关于Android Custom Views and XML attributes的主要内容,如果未能解决你的问题,请参考以下文章
Collection Views and Building Custom Layouts-备
Laravel eloquent order by custom 属性