标签:
尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!
炮兵镇楼
要在数量上统计中国菜的品种,在地域上毫无争议地划分菜系,在今天,是一件几乎不可能完成的事……Cut…………抱歉……忘吃药了,再来一遍。如果非要对自定义控件的流程进行一个简单的划分,我会尝试将其分为三大部分:控件的绘制、控件的测量和控件的交互行为。前面我们用了六节的篇幅和一个翻页的例子来对控件的绘制有了一个全新的认识但是我们所做出的所有例子都是不完美的,为什么这么说呢,还是先来看个sample:
- public class ImgView extends View {
- private Bitmap mBitmap;
-
- public ImgView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
-
- canvas.drawBitmap(mBitmap, 0, 0, null);
- }
-
-
- public void setBitmap(Bitmap bitmap) {
- this.mBitmap = bitmap;
- }
- }
这个例子呢非常简单,我们用它来模拟类似ImageView的效果显示一张图片,在MainActivity中我们获取该控件并为其设置Bitmap:
- public class MainActivity extends Activity {
- private ImgView mImgView;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- mImgView = (ImgView) findViewById(R.id.main_pv);
- Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);
- mImgView.setBitmap(bitmap);
- }
- }
此时运行效果如下:
很简单对吧,可是上面的代码其实是有个问题的,至于什么问题?我们待会再说,就看你通过前面我们的学习能不能发现了。这一节我们重点是控件的测量,大家不知道注意没有,这个系列文章的命名我用了“控件”而非“View”其实目的就是说明我们的控件不仅包括View也应该包含ViewGroup,当然你也可以以官方的方式将其分为控件和布局,不过我更喜欢View和ViewGroup,好了废话不说,我们先来看看View的测量方式,上面的代码中MainActivity对应的布局文件如下:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </LinearLayout>
既然我们的自定义View也算一个控件那么我们也可以像平时做布局那样往我们的LinearLayout中添加各种各样的其他控件对吧:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- </LinearLayout>
但是运行后你却发现我们的Button和TextView却没有显示在屏幕上,这时你可能会说那当然咯,因为我们的ImgViewlayout_width和layout_height均为match_parent,可是即便你将其改成wrap_content:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
-
- </LinearLayout>
结果也一样,这时你肯定很困惑,不解的主要原因是没有搞懂View的测量机制,在前面的几节中我们或多或少有提到控件的测量,也曾经说过Android提供给我们能够操纵控件测量的方法是onMeasure:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
默认情况下onMeasure方法中只是简单地将签名列表中的两个int型参数回传给父类的onMeasure方法,然后由父类的方法去计算出最终的测量值。但是,这里有个问题非常重要,就是onMeasure签名列表中的这两个参数是从何而来,这里可以告诉大家的是,这两个参数是由view的父容器,代码中也就是我们的LinearLayout传递进来的,很多初学Android的朋友会将位于xml布局文件顶端的控件称之为根布局,比如这里我们的LinearLayout,而事实上在Android的GUI框架中,这个LinearLayout还称不上根布局,我们知道一个Activity可以对应一个View(也可以是ViewGroup),很多情况下我们会通过Activity的setContentView方法去设置我们的View:
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
setContentView在Activity内的实现也非常简单,就是调用getWindow方法获取一个Window类型的对象并调用其setContentView方法:
- public void setContentView(int layoutResID) {
- getWindow().setContentView(layoutResID);
- initActionBar();
- }
而这个Window对象
- public Window getWindow() {
- return mWindow;
- }
其本质也就是一个PhoneWindow,在Activity的attach方法中通过makeNewWindow生成:
- final void attach(Context context, ActivityThread aThread,
-
-
- mWindow = PolicyManager.makeNewWindow(this);
- mWindow.setCallback(this);
- mWindow.getLayoutInflater().setPrivateFactory(this);
- if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
- mWindow.setSoftInputMode(info.softInputMode);
- }
- if (info.uiOptions != 0) {
- mWindow.setUiOptions(info.uiOptions);
- }
-
-
- }
在PolicyManager中通过反射的方式获取com.android.internal.policy.impl.Policy的一个实例:
- public final class PolicyManager {
- private static final String POLICY_IMPL_CLASS_NAME =
- "com.android.internal.policy.impl.Policy";
-
- private static final IPolicy sPolicy;
-
- static {
- try {
- Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
- sPolicy = (IPolicy)policyClass.newInstance();
- } catch (ClassNotFoundException ex) {
- throw new RuntimeException(
- POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
- } catch (InstantiationException ex) {
- throw new RuntimeException(
- POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
- } catch (IllegalAccessException ex) {
- throw new RuntimeException(
- POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
- }
- }
-
-
-
- public static Window makeNewWindow(Context context) {
- return sPolicy.makeNewWindow(context);
- }
-
-
- }
并通过其内部的makeNewWindow实现返回一个PhoneWindow对象:
- public Window makeNewWindow(Context context) {
- return new PhoneWindow(context);
- }
PhoneWindow是Window的一个子类,其对Window中定义的大量抽象方法作了具体的实现,比如我们的setContentView方法在Window中仅做了一个抽象方法定义:
- public abstract class Window {
-
-
- public abstract void setContentView(int layoutResID);
-
- public abstract void setContentView(View view);
-
- public abstract void setContentView(View view, ViewGroup.LayoutParams params);
-
-
- }
其在PhoneWindow中都有具体的实现:
- public class PhoneWindow extends Window implements MenuBuilder.Callback {
-
-
- @Override
- public void setContentView(int layoutResID) {
- if (mContentParent == null) {
- installDecor();
- } else {
- mContentParent.removeAllViews();
- }
- mLayoutInflater.inflate(layoutResID, mContentParent);
- final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
- }
- }
-
- @Override
- public void setContentView(View view) {
- setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
- }
-
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- if (mContentParent == null) {
- installDecor();
- } else {
- mContentParent.removeAllViews();
- }
- mContentParent.addView(view, params);
- final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
- }
- }
-
-
- }
当然如果你要是使用了TV的SDK那么这里就不是PhoneWindow而是TVWindow了,至于是不是呢?留给大家去验证。到这里我们都还没完,在PhoneWindow的setContentView方法中先会去判断mContentParent这个引用是否为空,如果为空则表示我们是第一次生成那么调用installDecor方法去生成一些具体的对象否则清空该mContentParent下的所有子元素(注意mContentParent是一个ViewGroup)并通过LayoutInflater将xml布局转换为View Tree添加至mContentParent中(这里根据setContentView(int layoutResID)方法分析,其他重载方法类似),installDecor方法做的事相对多但不复杂,首先是对DecorView类型的mDecor成员变量赋值继而将其注入generateLayout方法生成我们的mContentParent:
- private void installDecor() {
- if (mDecor == null) {
- mDecor = generateDecor();
-
- }
-
- if (mContentParent == null) {
- mContentParent = generateLayout(mDecor);
-
-
- }
-
-
- }
generateLayout方法中做的事就多了,简直可以跟performTraversals拼,这里不贴代码了简单分析一下,generateLayout方法中主要根据当前我们的Style类型为当前Window选择不同的布局文件,看到这里,想必大家也该意识到,这才是我们的“根布局”,其会指定一个用来存放我们自定义布局文件(也就是我们口头上常说的根布局比如我们例子中的LinearLayout)的ViewGroup,一般情况下这个ViewGroup的重任由FrameLayout来承担,这也是为什么我们在获取我们xml布局文件中的顶层布局时调用其getParent()方法会返回FrameLayout对象的原因,其id为android:id="@android:id/content":
- protected ViewGroup generateLayout(DecorView decor) {
-
-
- ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
-
-
- }
在这个Window布局文件被确定后,mDecor则会将该布局所生成的对应View添加进来并获取id为content的View将其赋给mContentParent,至此mContentParent和mDecor均已生成,而我们xml布局文件中的布局则会被添加至mContentParent。对应关系类似下图:
说了大半天才理清这个小关系,但是我们还没说到重点…………………………就是widthMeasureSpec和heightMeasureSpec究竟是从哪来的……………………如果我们不做上面的一个分析,很多童鞋压根无从下手,有了上面一个分析,我们知道我们界面的真正根视图应该是DecorView,那么我们的widthMeasureSpec和heightMeasureSpec应该从这里或者更上一层PhoneWindow传递进来对吧,但是DecorView是FrameLayout的一个实例,在FrameLayout的onMeasure中我们确实有对子元素的测量,但是问题是FrameLayout:onMeasure方法中的widthMeasureSpec和heightMeasureSpec又是从何而来呢?追溯上去我们又回到了View…………………………………………………………不了解Android GUI框架的童鞋迈出的第一步就被无情地煽了回去。其实在Android中我们可以在很多方面看到类似MVC架构的影子,比如最最常见的就是我们的xml界面布局——Activity等组件——model数据之间的关系,而在整个GUI的框架中,我们也可以对其做出类似的规划,View在设计过程中就注定了其只会对显示数据进行处理比如我们的测量布局和绘制还有动画等等,而承担Controller控制器重任的是谁呢?在Android中这一功能由ViewRootImpl承担,我们在前面提到过这个类,其负责的东西很多,比如我们窗口的显示、用户的输入输出当然还有关于处理我们绘制流程的方法:
- private void performTraversals() {
-
- }
performTraversals方法是处理绘制流程的一个开始,内部逻辑相当相当多&复杂,虽然没有View类复杂……但是让我选的话我宁愿看整个View类也不愿看performTraversals方法那邪恶的逻辑…………囧,在该方法中我们可以看到如下的一段逻辑(具体各类变量的赋值就不贴了实在太多):
- private void performTraversals() {
-
-
- if (!mStopped) {
-
-
- int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
- int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
-
-
-
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
-
-
- }
可以看到在performTraversals方法中通过getRootMeasureSpec获取原始的测量规格并将其作为参数传递给performMeasure方法处理,这里我们重点来看getRootMeasureSpec方法是如何确定测量规格的,首先我们要知道mWidth, lp.width和mHeight, lp.height这两组参数的意义,其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定,mWidth和mHeight表示当前窗口的大小,其值由performTraversals中一系列逻辑计算确定,这里跳过,而在getRootMeasureSpec中作了如下判断:
- private static int getRootMeasureSpec(int windowSize, int rootDimension) {
- int measureSpec;
- switch (rootDimension) {
-
- case ViewGroup.LayoutParams.MATCH_PARENT:
-
- measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
- break;
- case ViewGroup.LayoutParams.WRAP_CONTENT:
-
- measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
- break;
- default:
-
- measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
- break;
- }
- return measureSpec;
- }
也就是说不管如何,我们的根视图大小必定都是全屏的……
至此,我们算是真正接触到根视图的测量规格,尔后这个规格会被由上至下传递下去,并由当前view与其父容器共同作用决定最终的测量大小,在View与ViewGroup递归调用实现测量的过程中有几个重要的方法,对于View而言则是measure方法:
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
-
-
-
- if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
- widthMeasureSpec != mOldWidthMeasureSpec ||
- heightMeasureSpec != mOldHeightMeasureSpec) {
-
-
- mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
-
- resolveRtlPropertiesIfNeeded();
-
- int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
- mMeasureCache.indexOfKey(key);
- if (cacheIndex < 0 || sIgnoreMeasureCache) {
-
- onMeasure(widthMeasureSpec, heightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- } else {
- long value = mMeasureCache.valueAt(cacheIndex);
-
- setMeasuredDimension((int) (value >> 32), (int) value);
- mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
-
-
- if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
- throw new IllegalStateException("onMeasure() did not set the"
- + " measured dimension by calling"
- + " setMeasuredDimension()");
- }
-
-
- mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
- }
-
-
- mOldWidthMeasureSpec = widthMeasureSpec;
- mOldHeightMeasureSpec = heightMeasureSpec;
-
- mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
- (long) mMeasuredHeight & 0xffffffffL);
- }
可以看到,View对控件的测量是在onMeasure方法中进行的,也就是文章开头我们在自定义View中重写的onMeasure方法,但是我们并没有对其做任何的处理,也就是说保持了其在父类View中的默认实现,其默认实现也很简单:
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
- }
其直接调用了setMeasuredDimension方法为其设置了两个计算后的测量值:
- protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
-
-
-
- mMeasuredWidth = measuredWidth;
- mMeasuredHeight = measuredHeight;
-
-
- mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
- }
回到onMeasure方法,我们来看看这两个测量值具体是怎么获得的,其实非常简单,首先来看getSuggestedMinimumWidth方法:
- protected int getSuggestedMinimumWidth() {
- return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
- }
如果背景为空那么我们直接返回mMinWidth最小宽度否则就在mMinWidth和背景最小宽度之间取一个最大值,getSuggestedMinimumHeight类同,mMinWidth和mMinHeight我没记错的话应该都是100px,而getDefaultSize方法呢也很简单:
- public static int getDefaultSize(int size, int measureSpec) {
-
- int result = size;
-
-
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
-
-
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- result = size;
- break;
- case MeasureSpec.AT_MOST:
- case MeasureSpec.EXACTLY:
- result = specSize;
- break;
- }
- return result;
- }
注意上述代码中当模式为AT_MOST和EXACTLY时均会返回解算出的测量尺寸,还记得上面我们说的PhoneWindow、DecorView么从它们那里获取到的测量规格层层传递到我们的自定义View中,这就是为什么我们的View在默认情况下不管是math_parent还是warp_content都能占满父容器的剩余空间(这里面还有父布局LinearLayout的作用就先略过了了解即可)。上述onMeasure的过程则是View默认的处理过程,如果我们不喜欢Android帮我们处理那么我们可以自己重写onMeasure实现自己的测量逻辑:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- setMeasuredDimension(250, 250);
- }
最简单的粗暴的就是直接将两个值作为参数传入setMeasuredDimension方法,效果如下:
当然这样不好,用Android官方的话来说就是太过“专政”,因为它完全摒弃了父容器的意愿,完全由自己决定了大小,如果大家逛blog看技术文章或者听别人讨论常常会听到别人这么说view的最终测量尺寸是由view本身何其父容器共同决定的,至于如何共同决定我们呆会再说,这里我们先看看如何能在一定程度上顺应爹的“意愿”呢?从View默认的测量模式中我们可以看到它频繁使用了一个叫做MeasureSpec的类,而在ViewRootImpl中呢也有大量用到该类,该类的具体说明大家可以围观我早期的一篇文章:http://blog.csdn.net/aigestudio/article/details/38636531,里面有对MeasureSpec类的详细说明,这里我就简单概述下MeasureSpec类中的三个Mode常量值的意义,其中UNSPECIFIED表示未指定,爹不会对儿子作任何的束缚,儿子想要多大都可以;EXACTLY表示完全的,意为儿子多大爹心里有数,爹早已算好了;AT_MOST表示至多,爹已经为儿子设置好了一个最大限制,儿子你不能比这个值大,不能再多了!父容器所谓的“意图”其实就由上述三个常量值表现,既然如此我们就该对这三个Mode常量做一个判断才行,不然怎么知道爹的意图呢:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- int resultWidth = 0;
-
-
- int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
-
-
- int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
-
-
- if (modeWidth == MeasureSpec.EXACTLY) {
-
- resultWidth = sizeWidth;
- }
-
- else {
-
- resultWidth = mBitmap.getWidth();
-
-
- if (modeWidth == MeasureSpec.AT_MOST) {
-
- resultWidth = Math.min(resultWidth, sizeWidth);
- }
- }
-
- int resultHeight = 0;
- int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
- int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
-
- if (modeHeight == MeasureSpec.EXACTLY) {
- resultHeight = sizeHeight;
- } else {
- resultHeight = mBitmap.getHeight();
- if (modeHeight == MeasureSpec.AT_MOST) {
- resultHeight = Math.min(resultHeight, sizeHeight);
- }
- }
-
-
- setMeasuredDimension(resultWidth, resultHeight);
- }
如上代码所示我们从父容器传来的MeasureSpec中分离出了mode和size,size只是一个期望值我们需要根据mode来计算最终的size,如果父容器对子元素没有一个确切的大小那么我们就需要尝试去计算子元素也就是我们的自定义View的大小,而这部分大小更多的是由我们也就是开发者去根据实际情况计算的,这里我们模拟的是一个显示图片的控件,那么控件的实际大小就应该跟我们的图片一致,但是虽然我们可以做出一定的决定也要考虑父容器的限制值,当mode为AT_MOST时size则是父容器给予我们的一个最大值,我们控件的大小就不应该超过这个值。下面是运行效果:
如我所说,控件的实际大小需要根据我们的实际需求去计算,这里我更改一下xml为我们的ImgView加一个内边距值:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.ImgView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="20dp" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- </LinearLayout>
这时你会发现蛋疼了……毫无内边距的效果,而在这种情况下我们则需在计算控件尺寸时考虑内边距的大小:
- resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
- resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
这时我们就有了内边距的效果对吧:
诶、等等,好像不对啊,上边距和左边距为什么没有了?原因很简单,因为我们在绘制时并没有考虑到Padding的影响,下面我们更改一下绘制逻辑:
- @Override
- protected void onDraw(Canvas canvas) {
-
- canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingTop(), null);
- }
这时我们的内边距就完美了:
很多朋友问那Margin外边距呢??淡定,外边距轮不到view来算,Andorid将其封装在LayoutParams内交由父容器统一处理。很多时候我们的控件往往不只是一张简单的图片那么乏味,比如类似图标的效果:
一个图标常常除了一张图片外底部还有一个title,这时我们的测量逻辑就应该做出相应的改变了,这里我用一个新的IconView去做:
- public class IconView extends View {
- private Bitmap mBitmap;
- private TextPaint mPaint;
- private String mStr;
-
- private float mTextSize;
-
-
- private enum Ratio {
- WIDTH, HEIGHT
- }
-
- public IconView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
-
- calArgs(context);
-
-
- init();
- }
-
-
- private void calArgs(Context context) {
-
- int sreenW = MeasureUtil.getScreenSize((Activity) context)[0];
-
-
- mTextSize = sreenW * 1 / 10F;
- }
-
-
- private void init() {
-
- if (null == mBitmap) {
- mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
- }
-
-
- if (null == mStr || mStr.trim().length() == 0) {
- mStr = "AigeStudio";
- }
-
-
- mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
- mPaint.setColor(Color.LTGRAY);
- mPaint.setTextSize(mTextSize);
- mPaint.setTextAlign(Paint.Align.CENTER);
- mPaint.setTypeface(Typeface.DEFAULT_BOLD);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- setMeasuredDimension(getMeasureSize(widthMeasureSpec, Ratio.WIDTH), getMeasureSize(heightMeasureSpec, Ratio.HEIGHT));
- }
-
-
- private int getMeasureSize(int measureSpec, Ratio ratio) {
-
- int result = 0;
-
-
- int mode = MeasureSpec.getMode(measureSpec);
- int size = MeasureSpec.getSize(measureSpec);
-
-
- switch (mode) {
- case MeasureSpec.EXACTLY:
- result = size;
- break;
- default:
- if (ratio == Ratio.WIDTH) {
- float textWidth = mPaint.measureText(mStr);
- result = ((int) (textWidth >= mBitmap.getWidth() ? textWidth : mBitmap.getWidth())) + getPaddingLeft() + getPaddingRight();
- } else if (ratio == Ratio.HEIGHT) {
- result = ((int) ((mPaint.descent() - mPaint.ascent()) * 2 + mBitmap.getHeight())) + getPaddingTop() + getPaddingBottom();
- }
-
-
- if (mode == MeasureSpec.AT_MOST) {
- result = Math.min(result, size);
- }
- break;
- }
- return result;
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
-
- canvas.drawBitmap(mBitmap, getWidth() / 2 - mBitmap.getWidth() / 2, getHeight() / 2 - mBitmap.getHeight() / 2, null);
- canvas.drawText(mStr, getWidth() / 2, mBitmap.getHeight() + getHeight() / 2 - mBitmap.getHeight() / 2 - mPaint.ascent(), mPaint);
- }
- }
在xml文件中对其引用并加入一些系统自带的控件:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="50dp" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- </LinearLayout>
效果如下:
好了就先这样吧,上面我们曾说过View的测量大小是由View和其父容器共同决定的,但是上述源码的分析中我们其实并没有体现,因为它们都在ViewGroup中,这里我们就要涉及ViewGroup中与测量相关的另外几个方法:measureChildren、measureChild和measureChildWithMargins还有getChildMeasureSpec,见名知意这几个方法都跟ViewGroup测量子元素有关,其中measureChildWithMargins和measureChildren类似只是加入了对Margins外边距的处理,ViewGroup提供对子元素测量的方法从measureChildren开始:
- protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
- final int size = mChildrenCount;
- final View[] children = mChildren;
- for (int i = 0; i < size; ++i) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
measureChildren的逻辑很简单,通过父容器传入的widthMeasureSpec和heightMeasureSpec遍历子元素并调用measureChild方法去测量每一个子元素的宽高:
- protected void measureChild(View child, int parentWidthMeasureSpec,
- int parentHeightMeasureSpec) {
-
- final LayoutParams lp = child.getLayoutParams();
-
-
- final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
- mPaddingLeft + mPaddingRight, lp.width);
- final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
- mPaddingTop + mPaddingBottom, lp.height);
-
-
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
这里我们主要就是看看getChildMeasureSpec方法是如何确定最终测量规格的:
- public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
-
- int specMode = MeasureSpec.getMode(spec);
- int specSize = MeasureSpec.getSize(spec);
-
-
- int size = Math.max(0, specSize - padding);
-
-
- int resultSize = 0;
- int resultMode = 0;
-
-
- switch (specMode) {
- case MeasureSpec.EXACTLY:
-
- if (childDimension >= 0) {
-
- resultSize = childDimension;
-
-
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
-
- resultSize = size;
-
-
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
-
- resultSize = size;
-
-
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
-
- case MeasureSpec.AT_MOST:
-
- if (childDimension >= 0) {
-
- resultSize = childDimension;
-
-
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
-
- resultSize = size;
-
-
- resultMode = MeasureSpec.AT_MOST;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
-
- resultSize = size;
-
-
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
-
- case MeasureSpec.UNSPECIFIED:
-
- if (childDimension >= 0) {
-
- resultSize = childDimension;
-
-
- resultMode = MeasureSpec.EXACTLY;
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
-
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
-
- resultSize = 0;
- resultMode = MeasureSpec.UNSPECIFIED;
- }
- break;
- }
-
-
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }
至此我们可以看到一个View的大小由其父容器的测量规格MeasureSpec和View本身的布局参数LayoutParams共同决定,但是即便如此,最终封装的测量规格也是一个期望值,究竟有多大还是我们调用setMeasuredDimension方法设置的。上面的代码中有些朋友看了可能会有疑问为什么childDimension >= 0就表示一个确切值呢?原因很简单,因为在LayoutParams中MATCH_PARENT和WRAP_CONTENT均为负数、哈哈!!正是基于这点,Android巧妙地将实际值和相对的布局参数分离开来。那么我们该如何对ViewGroup进行测量呢?这里为了说明问题,我们自定义一个ViewGroup:
- public class CustomLayout extends ViewGroup {
-
- public CustomLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
- }
-
- }
ViewGroup中的onLayout方法是一个抽象方法,这意味着你在继承时必须实现,onLayout的目的是为了确定子元素在父容器中的位置,那么这个步骤理应该由父容器来决定而不是子元素,因此,我们可以猜到View中的onLayout方法应该是一个空实现:
- public class View implements Drawable.Callback, KeyEvent.Callback,
- AccessibilityEventSource {
-
-
-
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- }
-
-
- }
与View不同的是,ViewGroup表示一个容器,其内可以包含多个元素,既可以是一个布局也可以是一个普通的控件,那么在对ViewGroup测量时我们也应该对这些子元素进行测量:
- public class CustomLayout extends ViewGroup {
-
- public CustomLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-
- if (getChildCount() > 0) {
-
- measureChildren(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
- }
-
- }
然后我们在xml布局文件中替换原来的LinearLayout使用我们自定义的布局:
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="50dp" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- </com.aigestudio.customviewdemo.views.CustomLayout>
运行后你会发现没有任何东西显示,为什么呢?如上所说我们需要父容器告诉子元素它的出现位置,而这个过程由onLayout方法去实现,但是此时我们的onLayout方法什么都没有,子元素自然也不知道自己该往哪搁,自然就什么都没有咯……知道了原因我们就来实现onLayout的逻辑:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
-
- if (getChildCount() > 0) {
-
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- child.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
- }
- }
- }
逻辑很简单,如果有子元素那么我们遍历这些子元素并调用其layout方法告诉它们自己该在的位置,这里我们就直接让所有的子元素都从父容器的[0, 0]点开始到[getMeasuredWidth(), getMeasuredHeight()]父容器的测量宽高结束,这么一来,所有的子元素应该都是填充了父容器的对吧:
看到屏幕上的巨大Button我不禁吸了一口屁!这样的布局太蛋疼,全被Button一个玩完了还搞毛,可不可以像LinearLayout那样挨个显示呢?答案是肯定的!我们来修改下onLayout的逻辑:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
-
- if (getChildCount() > 0) {
-
- int mutilHeight = 0;
-
-
- for (int i = 0; i < getChildCount(); i++) {
-
- View child = getChildAt(i);
-
-
- child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);
-
-
- mutilHeight += child.getMeasuredHeight();
- }
- }
- }
可以看到我们通过一个mutilHeight来存储高度倍增值,每一次子元素布局完后将当前mutilHeight与当前子元素的高度相加并在下一个子元素布局时在高度上加上mutilHeight,效果如下:
是不是和上面LinearLayout效果有点一样了?当然LinearLayout的布局逻辑远比我们的复杂得多,我们呢也只是对其进行一个简单的模拟而已。大家注意到ViewGroup的onLayout方法的签名列表中有五个参数,其中boolean changed表示是否与上一次位置不同,其具体值在View的layout方法中通过setFrame等方法确定:
- public void layout(int l, int t, int r, int b) {
-
-
- boolean changed = isLayoutModeOptical(mParent) ?
- setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
-
-
- }
而剩下的四个参数则表示当前View与父容器的相对距离,如下图:
好了,说到这里想必大家对ViewGroup的测量也有一定的了解了,但是这必定不是测量过程全部,如我上面所说,测量的具体过程因控件而异,上面我们曾因为给我们的自定义View加了内边距后修改了绘制的逻辑,因为我们需要在绘制时考虑内边距的影响,而我们的自定义ViewGroup呢?是不是也一样呢?这里我给其加入60dp的内边距:
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="60dp"
- android:background="#FFFFFFFF"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="50dp" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- </com.aigestudio.customviewdemo.views.CustomLayout>
运行后效果如下:
内边距把我们的子元素给“吃”掉了,那么也就是说我们在对子元素进行定位时应该进一步考虑到父容器内边距的影响对吧,OK,我们重理onLayout的逻辑:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
- int parentPaddingLeft = getPaddingLeft();
- int parentPaddingTop = getPaddingTop();
-
-
- if (getChildCount() > 0) {
-
- int mutilHeight = 0;
-
-
- for (int i = 0; i < getChildCount(); i++) {
-
- View child = getChildAt(i);
-
-
-
- child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop, child.getMeasuredWidth() + parentPaddingLeft, child.getMeasuredHeight() + mutilHeight + parentPaddingTop);
-
-
- mutilHeight += child.getMeasuredHeight();
- }
- }
- }
此时的效果如下:
既然内边距如此,那么Margins外边距呢?我们来看看,在xml布局文件中为我们的CustomLayout加一个margins:
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="30dp"
- android:padding="20dp"
- android:background="#FF597210"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="AigeStudio" />
-
- </com.aigestudio.customviewdemo.views.CustomLayout>
效果如下:
OK,目测没什么问题,可是当我们为子元素设置外边距时,问题就来了……不管你怎么设都不会有任何效果,原因很简单,我们上面也说了,Margins是由父容器来处理,而我们的CustomLayout中并没有对其做任何的处理,那么我们应该怎么做呢?首先要知道Margins封装在LayoutParams中,如果我们想实现自己对其的处理那么我们必然也有必要实现自己布局的LayoutParams:
- public class CustomLayout extends ViewGroup {
-
-
-
- public static class CustomLayoutParams extends MarginLayoutParams {
-
- public CustomLayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- public CustomLayoutParams(android.view.ViewGroup.LayoutParams source) {
- super(source);
- }
-
- public CustomLayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- }
-
- public CustomLayoutParams(int width, int height) {
- super(width, height);
- }
- }
- }
我们在我们的CustomLayout中生成了一个静态内部类CustomLayoutParams,保持其默认的构造方法即可,这里我们什么也没做,当然你可以定义自己的一些属性或逻辑处理,因控件而异这里不多说了,后面慢慢会用到。然后在我们的CustomLayout中重写所有与LayoutParams相关的方法,返回我们自己的CustomLayoutParams:
- public class CustomLayout extends ViewGroup {
-
-
-
- @Override
- protected CustomLayoutParams generateDefaultLayoutParams() {
- return new CustomLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- }
-
-
- @Override
- protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
- return new CustomLayoutParams(p);
- }
-
-
- @Override
- public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new CustomLayoutParams(getContext(), attrs);
- }
-
-
- @Override
- protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
- return p instanceof CustomLayoutParams;
- }
-
-
- }
最后更改我们的测量逻辑:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- int parentDesireWidth = 0;
- int parentDesireHeight = 0;
-
-
- if (getChildCount() > 0) {
-
- for (int i = 0; i < getChildCount(); i++) {
-
-
- View child = getChildAt(i);
-
-
- CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
-
-
- measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
-
-
- parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
- parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
- }
-
-
- parentDesireWidth += getPaddingLeft() + getPaddingRight();
- parentDesireHeight += getPaddingTop() + getPaddingBottom();
-
-
- parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
- parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
- }
-
-
- setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
- int parentPaddingLeft = getPaddingLeft();
- int parentPaddingTop = getPaddingTop();
-
-
- if (getChildCount() > 0) {
-
- int mutilHeight = 0;
-
-
- for (int i = 0; i < getChildCount(); i++) {
-
- View child = getChildAt(i);
-
- CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
-
-
-
- child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin, child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin, child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);
-
-
- mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
- }
- }
- }
布局文件如下:
- <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="#FF597210"
- android:orientation="vertical" >
-
- <com.aigestudio.customviewdemo.views.IconView
- android:id="@+id/main_pv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="10dp"
- android:layout_marginLeft="20dp"
- android:layout_marginRight="30dp"
- android:layout_marginTop="5dp" />
-
- <Button
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
- android:layout_marginLeft="2dp"
- android:layout_marginRight="8dp"
- android:layout_marginTop="4dp"
- android:text="AigeStudio" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="28dp"
- android:layout_marginLeft="7dp"
- android:layout_marginRight="19dp"
- android:layout_marginTop="14dp"
- android:background="#FF166792"
- android:text="AigeStudio" />
-
- </com.aigestudio.customviewdemo.views.CustomLayout>
运行效果如下:
~~~~~~~~好了好了、不讲了,View的基本测量过程大致就是这样,如我所说测量并不是定式的过程,总会因控件而已,我们在自定义控件时要准确地测量,一定要准确,测量的结果会直接影响后面的布局定位、绘制甚至交互,所以马虎不得,你也可以看到Android给我们提供的LinearLayout、FrameLayout等布局都有极其严谨的测量逻辑,为的就是确保测量结果的准确。
本篇幅虽长,但是我们其实就讲了三点:
- 一个界面窗口的元素构成
- framework对View测量的控制处理
- View和ViewGroup的简单测量
好了、不说了、实在说不动了………………到此为止&¥……#¥……%#¥%#¥%#%¥哦!对了,文章开头我给各位设了一个问题,不知道大家发现没有,本来说这节顺带讲了,看着篇幅太长下节再说吧……
源码下载:传送门
自定义控件其实很简单7/12
标签:
原文地址:http://www.cnblogs.com/aprz512/p/4598180.html