Fork me on GitHub

《Android 50 Hacks》读书笔记-布局篇

最近在http://www.it-ebooks.info/看见了一本《50 Android Hacks》,感觉还好.在这里写一下读书笔记.

前言:

我不知道作为一个没过四级的人是怎么看完这本书的.记得以前英语考试时,读阅读时总是不耐心,读着读着就烦气了.后来的考试总是喜欢看着答案蒙.至于现在为什么能耐心阅读关于计算机的一些英文文档,可能就是我对自己有野心,对程序员这个行业有野心吧.希望有一天我能骄傲地跟别人说我是一个程序员.

Hack 1 Centering views using weights (Android v1.6+)

What should I write if I want a but- ton to be centered and 50% of its parent width?

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#FFFFFF"
android:gravity="center"
android:orientation="horizontal"
android:weightSum="1">
<Button
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="0.5"
    android:text="Click me"/>
</LinearLayout>

注意LinearLayout中的属性 android:weightSum . 默认情况下weightSum的值是父控件里面所有child的数量.

上面这段代码实现了button的width是父控件的50%.

计算公式:

Button’s width + Button’s weight * 200 / sum(weight);(假设LinearLayout的宽度是200)

也就是 控件宽度+父控件剩余宽度*比例 (首先按照控件声明的尺寸进行分配,然后剩下的尺寸按照weight分配)

Hack 2 Using lazy loading and avoiding replication (Android v1.6+)

这个技巧将会学到:

  • include 标签避免xml代码的重复
  • 使用ViewStub Class实现视图的懒加载

2.1 Avoid replication using the tag

我们想象一下在我们应用的每一个页面中添加一个同样的页脚,例如一个只带有应用名字的TextView.如果我们有很多个Activity,很多个xml文件.我们该怎么编辑它们?复制粘贴将会解决这个问题,但是看上去不是高效的.一个简单的途径就是用 include 标签添加这个页脚到我们应用里.我们的其中一个Activity的xml文件就可以像这样:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:gravity="center_horizontal"
    android:text="@string/hello"/>
  <include layout="@layout/footer"/>
</RelativeLayout/>

footer的xml代码:

 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="30dp"
        android:gravity="center_horizontal"
        android:text="@string/footer_text"/>

在第一个例子里面,我们在单独的footer.xml定义属性.可当Activity的xml中父控件是LinearLayout呢?我们再在footer.xml定义上面这些属性就无效了,因为像android:layout_marginBottom这些属性是在RelativeLayout才有效的.下面介绍第二种方式,在 include 标签内定义android:layout_*这些属性.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center_horizontal"
            android:text="@string/hello"/>

        <include
            layout="@layout/footer"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="30dp"/>

修改过后的footer.xml:

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:gravity="center"
            android:text="@string/footer_text"/>

注意footer.xml中的宽和高都要设置才能被有效覆盖. #### 2.2 Lazy loading views with the ViewStub class

ViewStub Class:

A ViewStub is an invisible, zero-sized View that can be used to lazily inflate layout resources at runtime. When a ViewStub is made visible, or when inflate() is invoked, the layout resource is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views.

你已经知道了ViewStub是什么,让我们来看一下该怎么用.在下面的例子中,你将会用一个ViewStub去懒加载 MapView .想象创建一个用来描述地方详细信息的View.我们来看以下两种情况:

  • 一些地区没有GPS信息
  • 用户可能不需要地图展示

SampleCode:

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

        <Button
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/show_map"
            android:onClick="onShowMap"/>

        <ViewStub
            android:id="@+id/map_stub"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout="@layout/map"
            android:inflatedId="@+id/map_view"/>
    </RelativeLayout>

很明显,我们在Activity中用map_stub得到ViewStub,layout属性告诉ViewStub应该去加载哪个布局文件.

map.xml代码如下:

<com.google.android.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:clickable="true"
        android:apiKey="my_api_key"/>

我们需要讨论的是最后这个属性android:inflatedId.我们可以用 inflate() 或者 setVisibility 方法让这个View显示出来.在这个例子中,我们用了 setVisibility(View.VISIBLE) ,因为我们不要对这个MapView做任何事情.如果我们需要得到一个引用, inflate() 会返回这个View避免第二次去调用 findViewById() .

Activity 代码:

 public class MainActivity extends MapActivity {
        private View mViewStub;


        @Override public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            mViewStub = findViewById(R.id.map_stub);
        }


        public void onShowMap(View v) {
            mViewStub.setVisibility(View.VISIBLE);
        }


        ...
    }

就像你看见的一样,是否展示这个地图只需要改变ViewStub的 visibility 变量值即可.

Hack 3 Creating a custom ViewGroup (Android v1.6+)

当你正在设计App时,可能在页面中会有复杂的视图去展示.想象你正在创建一个纸牌游戏,并像下图的那样展示手牌.该怎么创建这个视图呢?

可以在RelativeLayout中设置margin,这是可行的.代码如下:

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

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#FF0000"/>

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:layout_marginLeft="30dp"
            android:layout_marginTop="20dp"
            android:background="#00FF00"/>

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:layout_marginLeft="60dp"
            android:layout_marginTop="40dp"
            android:background="#0000FF"/>
    </RelativeLayout>

效果是介个样子的:

下面介绍另外一种方式来创建相同类型的布局–创建一个自定义 ViewGroup .相比在xml中手动添加margins,自定义 ViewGroup 的优点有:

  • It’s easier to maintain if you’re using it in differ- ent activities.
  • You can use custom attributes to customize the position of the ViewGroup chil- dren.
  • The XML will be easier to understand because it’ll be more concise.
  • If you need to change the margins, you won’t need to recalculate by hand every child’s margin.

3.1 Understanding how Android draws views

Drawing the layout is a two-pass process: a measure pass and a layout pass. The measuring pass is implemented in measure(int, int) and is a top-down traversal of the View tree. Each View pushes dimension specifications down the tree during the recursion. At the end of the measure pass, every View has stored its measurements. The second pass happens in layout(int, int, int, int) and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.

为了理解这个概念,我们来分析一下绘制 ViewGroup 的方式.第一步在 onMeasure() 这个方法里面去测量宽和高.在这个方法里面, ViewGroup 会通过里面的子视图来计算它的大小.第二步通过在 onLayout() 这个方法里面去放置它的子视图.在这个方法里面, ViewGroup 通过在 onMeasure() 方法里收集到子视图的信息来放置它们.

3.2 Creating the CascadeLayout

xml代码:

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

    <com.manning.androidhacks.hack003.view.CascadeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        cascade:horizontal_spacing="30dp"
        cascade:vertical_spacing="20dp">

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#FF0000"/>

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#00FF00"/>

        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#0000FF"/>
    </com.manning.androidhacks.hack003.view.CascadeLayout>
</FrameLayout>

接着,我们来定义这些自定义属性.在 res/values 这个文件夹下创建 attrs.xml .代码:

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

        <declare-styleable name="CascadeLayout">

            <attr
                name="horizontal_spacing"
                format="dimension"/>

            <attr
                name="vertical_spacing"
                format="dimension"/>
        </declare-styleable>
    </resources>

res/values/dimens.xml 中定义自定义属性的值.

 <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <dimen name="cascade_horizontal_spacing">10dp</dimen>
        <dimen name="cascade_vertical_spacing">10dp</dimen>
    </resources>

在理解完Android怎样绘制视图后,你可能该想着去创建一个 CascadeLayout 的类去继承 ViewGroup ,在这个类里面去重写 onMeasure()onLayout() 这两个方法.因为代码有一点长,我们分三部分来分析:构造方法, onMeasure() , onLayout() . 下面是构造方法的代码:

public class CascadeLayout extends ViewGroup {
    private int mHorizontalSpacing;
    private int mVerticalSpacing;

    // Constructor called when view instance is created from an XML file.
    public CascadeLayout(Context context, AttributeSet attrs) {
        // mHorizontalSpacing and mVerticalSpacing are read from custom attributes. If they’re not present, use default values.
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);
        try {
            mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_horizontal_spacing,
                    getResources().getDimensionPixelSize(R.dimen.cascade_horizontal_spacing));
            mVerticalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing,
                    getResources().getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
        } finally {
            a.recycle();
        }
    }

    ...

onMeasure() 这个方法里面写代码前,我们创建一个自定义的 LayoutParams 类,作为 CascadeLayout 的内部类,为每一个子view保留x,y的坐标值.代码:

public static class LayoutParams extends ViewGroup.LayoutParams {
        int x;
        int y;


        public LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);
        }


        public LayoutParams(int w, int h) {
            super(w, h);
        }
    }

为了去使用 CascadeLayout.LayoutParams 这个类,我们需要在 CascadeLayout 这个类中重写一些额外的方法: checkLayoutParams() , generateDefaultLayoutParams() , generateLayoutParams(AttributeSet attrs) , generateLayoutParams(ViewGroup.LayoutParams p) .代码:

    @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }


    @Override protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }


    @Override public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }


    @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p.width, p.height);
    }

onMeasure() ,是这个类的关键.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	     // Use width and height to calculate layout’s final size and children’s x and y positions.
        int width = 0;
        int height = getPaddingTop();
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            // Make every child measure itself.
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            width = getPaddingLeft() + mHorizontalSpacing * i;
            lp.x = width;
            lp.y = height;
            width += child.getMeasuredWidth();
            height += mVerticalSpacing;
        }
        width += getPaddingRight();
        height += getChildAt(getChildCount() - 1).getMeasuredHeight() + getPaddingBottom();
        // Uses calculated width and height to set measured dimensions of whole layout.
        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
    }

最后一步是创建 onLayout() .

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
        }
    }

3.3 Adding custom attributes to the children

在最后一个章节中,你将会学到如何为子视图添加自定义属性.例如,为一个特定的子视图重新定义垂直间距.如下图

添加新的属性到 attrs.xml 中:

<declare-styleable name="CascadeLayout_LayoutParams">
        <attr name="layout_vertical_spacing"
            format="dimension"/>
    </declare-styleable>

因为这个属性名是以 *layout_* 开始的,所以它被添加到 *LayoutParams* 属性中.在 *LayoutParams* 这个构造方法中取出这个属性.

 public LayoutParams(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout_LayoutParams);
        try {
            verticalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,
                    -1);
        } finally {
            a.recycle();
        }
    }

verticalSpacing 是一个 public field .我们会在 CascadeLayout 中用到,如果子视图的 LayoutParams 中包含 verticalSpacing ,我们就可以使用它.

 verticalSpacing = mVerticalSpacing;
        ...
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp.verticalSpacing >= 0) {
            verticalSpacing = lp.verticalSpacing;
        }
        ...
        width += child.getMeasuredWidth();
        height += verticalSpacing;

Hack 4 Preferences hacks (Android v1.6+)

定义一个偏好设置界面,只需要写一些xml,就可以快速生成.大家看代码就可以懂了,跟自定义view类似.

效果图:

res/xml 中创建 prefs.xml .

<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright (c) 2012 Manning
  See the file license.txt for copying permission.
-->

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:key="pref_first_preferencescreen_key"
    android:title="Preferences">

    <PreferenceCategory
        android:title="User">

        <EditTextPreference
            android:key="pref_username"
            android:summary="Username:"
            android:title="Username"/>

    </PreferenceCategory>

    <PreferenceCategory
        android:title="Application">

        <Preference
            android:key="pref_rate"
            android:summary="Rate the app in the store!"
            android:title="Rate the app"/>

        <Preference
            android:key="pref_share"
            android:summary="Share the app with your friends"
            android:title="Share it"/>

        <com.manning.androidhacks.hack004.preference.EmailDialog
            android:dialogIcon="@drawable/ic_launcher"
            android:dialogTitle="Send Feedback"
            android:dialogMessage="Do you want to send an email with feedback?"
            android:key="pref_sendemail_key"
            android:negativeButtonText="Cancel"
            android:positiveButtonText="OK"
            android:summary="Send your feedback by e-mail"
            android:title="Send Feedback"/>

        <com.manning.androidhacks.hack004.preference.AboutDialog
            android:dialogIcon="@drawable/ic_launcher"
            android:dialogTitle="About"
            android:key="pref_about_key"
            android:negativeButtonText="@null"
            android:title="About"/>

    </PreferenceCategory>

</PreferenceScreen>

xml会创建好视图,我们只需在activity中写好逻辑即可.不一样的是,activity需要继承 android.preference.PreferenceActivity .

public class MainActivity extends PreferenceActivity implements
    OnSharedPreferenceChangeListener {

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.prefs);

    Preference sharePref = findPreference("pref_share");
    Intent shareIntent = new Intent();
    shareIntent.setAction(Intent.ACTION_SEND);
    shareIntent.setType("text/plain");
    shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Check this app!");
    shareIntent.putExtra(Intent.EXTRA_TEXT,
        "Check this awesome app at: ...");
    sharePref.setIntent(shareIntent);

    Preference ratePref = findPreference("pref_rate");
    Uri uri = Uri.parse("market://details?id=" + getPackageName());
    Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri);
    ratePref.setIntent(goToMarket);

    updateUserText();
  }

  @Override
  protected void onResume() {
    super.onResume();

    getPreferenceScreen().getSharedPreferences()
        .registerOnSharedPreferenceChangeListener(this);

  }

  @Override
  protected void onPause() {
    super.onPause();

    getPreferenceScreen().getSharedPreferences()
        .unregisterOnSharedPreferenceChangeListener(this);
  }

  @Override
  public void onSharedPreferenceChanged(
      SharedPreferences sharedPreferences, String key) {

    if (key.equals("pref_username")) {
      updateUserText();
    }
  }

  private void updateUserText() {
    EditTextPreference pref;
    pref = (EditTextPreference) findPreference("pref_username");
    String user = pref.getText();

    if (user == null) {
      user = "?";
    }

    pref.setSummary(String.format("Username: %s", user));
  }
}

自定义一个 preferences 就像自定义view一样.为了更好地理解它,我们来看一下 EmailDialog 这个类的代码.

// Custom class should extend some of existing preferences widgets. In this case, we’ll use DialogPreference.

public class EmailDialog extends DialogPreference {
  Context mContext;

  public EmailDialog(Context context) {
    this(context, null);
  }

  public EmailDialog(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public EmailDialog(Context context, AttributeSet attrs, int defStyle) {

  // Constructors are the same as those used to create a custom view extending the View class.

    super(context, attrs, defStyle);
    mContext = context;
  }

  // onClick() is overridden. If users press OK button, then we’ll launch email Intent with helper class.

  @Override
  public void onClick(DialogInterface dialog, int which) {
    super.onClick(dialog, which);

    if (DialogInterface.BUTTON_POSITIVE == which) {
      LaunchEmailUtil.launchEmailToIntent(mContext);
    }
  }
}

好了,以上就是这本书的第一章了,完成了4/50.有什么不对的,或者建议,大家可以直接留言给我,也可以给我发邮件.

15年就这么过去了,元旦那天朋友圈里,公众号里大家都在忙着自我检讨.想了想去年除了像每年一样都感觉孤独外,还有就是焦虑,每天都在焦虑.这一年有努力,有懈怠,有期待,有迷茫.知乎上的名字是taken2016,还没来得及改.记得当初起这名是想16年是收获的一年,而如今已经步入16年了.今年真的会是收获的一年咩?实在不行就换成taken2017吧:)不断积累,期待着,期待着.

好了,写了点没用的.希望写的东西对大家有用,非常期待大家对我的建议,无论是生活上还是技术上.毕竟我还是个刚入职不到一年的小菜鸟~~

有的朋友给我发邮件说这书太老,已经没有翻译的价值,况且世面上已经有中文版了.我去找了下确实,而且还有相应的中文PDF.所以我不打算再写后边的关于这书的篇章了.

有兴趣的小伙伴可以边看边参考完整的代码.本书github项目地址

最后,祝好:)