首页 > 代码库 > Enhancing Android UI with Custom Views 通过自定义view来让你的UI更屌!
Enhancing Android UI with Custom Views 通过自定义view来让你的UI更屌!
- Custom View
- View Measurement
- View Drawing
- Custom Attributes
- Custom ViewGroup
- ViewGroup Measurement
- Layout
- ViewGroup Drawing
- More Custom Attributes
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.
译文:能够构建自己的UI组件对你来说有很大的优势,比如你可以完全控制你的内容的显示样式。但成为一个自定义视图专家的最好理由之一,就是你将有能力使自己的视图层级结构变得扁平化。
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.
译文:一个自定义视图可以用来做几个系统原生视图合在一起才能做的事情。视图层级结构中的view视图越少,你的app也会运行的越流畅。
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.
译文:我们的第一个例子是做一个简单的小控件,它显示的是一对重叠的logo图片,logo右边是文本,并且左边图片和右边文本在竖直方向上呈居中对称。你可能会使用类似这样的一个小控件来表示体育比赛中的比分情况。
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.
译文:在一个视图层级可以被绘制之前,Android框架的第一个任务就是测量是否合格。在这个步骤中,一个层级结构中的所有视图将自上而下地被测量一遍;意思是从根视图开始测量,然后是它的孩子,接着是它孩子的孩子,以此类推,直到测量到每个子视图。
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.
译文:当一个父视图要求其子视图更新它的测量尺寸时,该子视图将会回调onMeasure()方法。每个子视图必须根据其父视图所给定的约束条件来设置自己的尺寸大小,并且通过调用setMeasuredDimension()方法来存储这些设置的测量值。如果不这样做的话将会导致异常发生。
@Override 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.
译文:在onMeasure()方法中,有两个int类型的包装过的参数值,这就是传说中的MeasureSpec(测量规格),每个视图应该根据MeasureSpec来确定该如何设置它的尺寸大小。MeasureSpec简单来说只是一个测量模式的标记值,实际上它是一个32位的int值,其中高2位是测量的模式,低30位是测量的大小。
There are three possible values for a spec‘s mode: UNSPECIFIED
, AT_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 指的是视图只能设置为规定的尺寸大小。
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.
译文:在使用wrap_content来布局你的view视图的情况下,另一个可能比较重要的事情是提供你期望的大小的测量值。下面就是我们这个例子中用来计算期望的宽度的方法。我们先获取到这个视图中三个主要元素的宽度值,然后返回绘制出重叠logo图片和文字所需要的总的宽度空间大小。
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.
译文:同样的,这也是我们的例子中用来计算view的期望高度所使用的方法。在高度方向上测量时,这完全由图像内容所控制,所以我们不需要注意文本元素。
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.
温馨提示:宁愿选择效率,而不选择灵活!不要把时间花在测试和重复一个你不需要的情况。与系统框架里的原生控件不同的是,你的自定义视图只需要满足您的app的实际需求就可以了。简单来说,就是先把你的自定义视图放在它最终要放的布局内,然后检查系统框架根据MeasureSpecs测量规格给的值,最后构建测量代码来处理这些特定的情况。
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.
译文:自定义视图的另一个主要工作是绘制出它的内容。为此,通过复写onDraw()方法,你将得到一个空白的canvas画布。这个画布的大小和位置已经根据你测量好的view指定好了,所以画布的坐标原点与你的view视图的左上角是重合的。该画布可以用来画各种形状,颜色,文本,bitmap等等。
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.
译文:许多系统框架组件诸如图片和文本布局等都提供了自己的draw()方法,来把他们的内容直接直接渲染到画布上去;在本例中我们就利用了这个特点。
@Override protected void onDraw(Canvas canvas) { if (mLeftDrawable != null) { mLeftDrawable.draw(canvas); } if (mTextLayout != null) { canvas.save(); canvas.translate(mTextOrigin.x, mTextOrigin.y); mTextLayout.draw(canvas); canvas.restore(); } if (mRightDrawable != null) { mRightDrawable.draw(canvas); } }
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.
译文:你可能会发现自己希望从XML布局文件中提取属性来设置到你的自定义视图上。我们可以通过在资源文件中声明style-able代码块来做到这一点。这个代码块必须包含所有我们想要从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"?> <resources> <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" /> </declare-styleable> </resources>
<com.example.customview.widget.DoubleImageView android:id="@+id/image1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableLeft="@drawable/flag_us" android:drawableRight="@drawable/flag_uk" android:textColor="#FFF" android:textSize="32sp" android:text="5 - 5" android:spacing="15dp"/>
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.
译文:在创建视图的过程中,我们使用obtainStyledAttributes()方法来提取style-able代码块中定义的属性值。该方法返回一个TypedArray实例,它让我们可以根据指定的类型来检索拿到相应的属性值,不管是图片类型,尺寸类型还是颜色类型。
DON‘T FORGET: TypedArrays are heavyweight objects that should be recycled immediately after all the attributes you need have been extracted.
切记:TypedArray是一个重量级对象,在你提取出所需的所有属性之后应立即回收它。
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) { setLeftDrawable(d); } d = a.getDrawable(R.styleable.DoubleImageView_android_drawableRight); if (d != null) { setRightDrawable(d); } int spacing = a.getDimensionPixelSize( R.styleable.DoubleImageView_android_spacing, 0); setSpacing(spacing); int color = a.getColor(R.styleable.DoubleImageView_android_textColor, 0); mTextPaint.setColor(color); int rawSize = a.getDimensionPixelSize( R.styleable.DoubleImageView_android_textSize, 0); mTextPaint.setTextSize(rawSize); CharSequence text = a.getText(R.styleable.DoubleImageView_android_text); setText(text); a.recycle(); }
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,它将它的所有子视图放置在相等间隔的3x3网格中。其实可以通过一层层地嵌套LinearLayouts…搞一个层级很深的结构来达到这样的效果。然而,我们只需要一点点的努力就可以大大降低这种结构层级,从而使性能更加流畅。
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.
译文:和view一样,viewgroup也要测量他们自己。对于本例来说,我们计算ViewGroup的尺寸,用的是系统框架提供的getDefaultSize()方法,它实质上返回了所有情况下的由MeasureSpec提供的尺寸值,除非它的父控件强加给它一个精确的尺寸值。
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.
译文:ViewGroup在测量过程中还有一个额外工作要做,那就是它还必须告诉其所有子视图来测量自己。我们想要每个子视图占用整个容器高度和宽度的1/3。这是通过构造一个新的MeasureSpec对象来完成的,这个对象包含了计算好的视图大小和设置为EXACTLY的测量模式。这就是要告知每个子视图,他们必须要按我们给的精确尺寸来设置测量值。
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()方法来设置好自己的测量尺寸。
@Override 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, MeasureSpec.EXACTLY); 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.
译文:测量工作做完之后,viewgroup还需要通过onLayout()回调来设置其子视图的边界范围。因为我们这里都是固定尺寸的网格,所以相对来说比较简单。我们首先可以根据index来确定一个视图应该在哪一行哪一列。然后我们可以调用子视图的layout()方法来设置其左,右,上,下值。
@Override 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(); child.layout(left, top, 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.
译文:注意,在子视图的layout()方法内部,我们可以使用子视图的getMeasuredWidth()和getMeasuredHeight()方法。因为在layout布局之前就已经测量过了,因此在这个阶段这些方法是永远有效的。而且这也是一个很方便的设置每个子视图边界的途径。
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.
温馨提示:测量和布局既可以很简单,也可以很复杂的,就看你怎么做。你很容易在试图处理所有可能影响你如何布局子视图的配置变化中迷失自己。你只要坚持为那些app真正会碰到的情况而写代码就可以了。
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.
译文:虽然viewgroup通常不用绘制自己的任何内容,但是在很多情况下,这可能是有用的。这里有两个让ViewGroup来绘制的有用的例子。
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.
译文:第一个例子是在dispatchDraw()方法内部,在super.dispatchDraw()方法被调用之后,在这个阶段,子视图已经绘制出来了,我们可以在他们之上做额外的绘制了。在我们的示例中,我们就利用这个来绘制子视图之上的网格线。
@Override protected void dispatchDraw(Canvas canvas) { //Let the framework do its thing super.dispatchDraw(canvas); //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.
译文:第二个例子是使用之前我们看到过的和view相同的onDraw()回调方法。我们在这里绘制的任何东西都会在子视图被绘制出来之前,因此这些绘制出来的东西将会出现在子视图的下面。这个特点对于绘制任何类型的动态背景或状态选择器是很有帮助的。
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.
译文:如果你想在ViewGroup的onDraw()方法里写任何代码,记住,你还需要调用一下setWillNotDraw(false)来使绘制的回调方法onDraw()可用。否则的话,你的onDraw()方法将永远不会被触发。这是因为viewgroup自动绘制是默认禁用的。
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?
译文:回到属性这个话题,如果我们想给view视图定义的属性在系统中并不存在,而且尝试和重用一个属性来用于不同的目的这也显得很尴尬,那这时我们该怎么办?
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.
译文:在这种情况下,我们可以在style-able代码块中自己定义新的属性。唯一的区别是,现在我们还必须要定义属性要表示的数据的类型;这在之前我们是不需要做的,因为系统框架已经定义好了。
Here, we are defining a dimension and color attribute to provide the styling for the box‘s grid lines via XML.
译文:在这里,我们通过XML文件定义了一个尺寸和颜色属性,来为这个3x3网格线做样式描述。
<?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" /> </declare-styleable> </resources>
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.
译文:现在,我们可以在我们布局中引用这些外部定义的属性了。需要注意的是,在我们自己app的包中定义的属性需要一个单独的命名空间,来指向我们内部的APK资源。
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布局文件中直接添加子视图。
<?xml version="1.0" encoding="utf-8"?> <com.example.customview.widget.BoxGridLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:separatorWidth="1dp" app:separatorColor="#CCC" app:numColumns="4"> … </com.example.customview.widget.BoxGridLayout>
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.
译文:纯粹为了好玩的缘故,我们甚至可以在这个布局本身内部再包含这个布局,从而创建出一个完整的9x9网格的效果,正如你在前面看到的那个截图一样。我们还定义了一个稍微厚一点的网格分割线从而可以区分大的方块和较小的方块。
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.customview.widget.BoxGridLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" app:separatorWidth="2dp" app:numColumns="2"> <include layout="@layout/box_small" /> <include layout="@layout/box_small" /> <include layout="@layout/box_small" /> <include layout="@layout/box_small" /> </com.example.customview.widget.BoxGridLayout> </FrameLayout>
Thanks!
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.
译文:我希望现在你可以发现,开始构建自定义视图和布局是一件多么简单的事情。减少对系统框架原生控件的依赖,将使你做出更加友好的UI界面,同时也会使你的视图层级机构更合理规范。你的用户和设备都将因此而感谢你。
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.
一定要去看看我的github,里面除了有我这里讲的所有示例之外还有些其他示例,他们能帮助你更好地构建自定义view。
Thanks for your time today, and I hope you learned something new!
译者注:
大家好!这是我的第一篇翻译博客,之所以选这篇文章,是因为它对我理解view的原理以及自定义view有过很大帮助,现翻译出来希望能够帮助到更多安卓开发爱好者。由于本人水平有限,难免会有错漏之处,如您有所发现,还请不吝指正,谢谢!
原文链接:<https://newcircle.com/s/post/1663/tutorial_enhancing_android_ui_with_custom_views_dave_smith_video>
Enhancing Android UI with Custom Views 通过自定义view来让你的UI更屌!