您的位置:首页 > 移动开发 > Android开发

读《50 Android Hacks》笔记整理Hack 1~Hack 8

2015-11-26 16:51 274 查看
[TOC]

最近看了《打造高质量Android应用》这本书,感觉是一本值得看看的书,不过里面也用挺多感觉冗余的地方。所以进行整理,等有时间我再想看的时候就不用在浪费时间了。

第一章 活用布局

Hack 1 使用weight属性实现视图的居中显示

需求与介绍:

因为不同Android设备的尺寸往往是不同的,所以我们需要进行不同尺寸屏幕的适配。硬编码是推荐的,因此需要使用其他方法来组织视图。

这个方法就是合用layout_weight和weightSum这两个属性来填充布局内部的任意剩余空间。

android:weightSum在开发文档中的解释如下:

“定义weight总和的最大值。如果未指定该值,以所有子视图的layout_weight属性的累加值作为综合的最大值。“

我们在使用layout_weight属性的时候一般会把layout_width或layout_height的值设置为0。因为,LinearLayout中的layout_weight属性,首先按照控件声明的尺寸进行分配,然后再将剩下的尺寸按weight分配。

计算方式=(总距离-控件声明的尺寸)值按weight分配。

而如果父控件设置了android:weightSum属性,layout_weight所属控件的width就由weightSum来决定。

示例:

以宽为200dp,android:weightSum属性值为1的LinerarLayout为例分析。计算这个控件的宽度公式如下:

Button’s width + Button’s weight * 200 / sum(weight)

因为指定了宽度为0dp,weight为0.5,sum(weight)等于1,所以结果如下:

0 +0.5 * 200 / 1 =100

外链地址

Hack 2 使用延迟加载以及避免代码重复

2.1 使用标签避免代码重复

需求与使用:

如我们需要为应用程序中每一个视图都添加一个页脚,它们都是相同的。这就可以通过标签把其他XML文件中定义的布局插入当前布局文件中。

使用的两种方法:

1.

//主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="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:text="hello" />

<include layout="@layout/footer_with_layout_properties"/>

</RelativeLayout>

//footer_with_layout_properties.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="30dp"
android:gravity="center_horizontal"
android:text="world" />


缺点:

android:layout_alignParentBottom=”true”属性是RelativeLayout的如果在LinearLayout中就不起作用了。

2.直接在标签里使用android:layout_*属性

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

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

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

</RelativeLayout>

//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="world" />


需要注意:

如果想在标签中覆盖被包含布局所指定的任何android:layout_*属性,必须标签中同时指定android:layout_width和android:layout_height这两个属性。

在方法二中我们把layout_width和ayout_height这两个属性都指定为0,目的是这样可以由使用footer.xml文件的人在标签中指定layout_width和ayout_height属性。如果不指定默认是0,就看不到页脚。

2.2 通过ViewStub实现View的延迟加载

需求与使用:

我们如果想要一个视图只在需要的时候显示就可以使用ViewStub这个类

在开发文档中的介绍:

“ViewStub是一种不可视并且大小为0点视图,可以延迟到运行时填充(inflate)布局资源。当ViewStub设置为可视或者inflate()方法被调用后,就会填充布局资源,然后ViewStub便会被填充的视图替代。“

示例:这里是延迟加载地图加载地图

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

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

</RelativeLayout>

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

//MainActivity.java
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);
}
...
}


在布局文件main.xml中通过map_stub这个id从Activity中获取ViewStub。同时,以android:layout属性指定需要填充的布局文件。
inflatedId是调用ViewStub的inflate()方法或setVisibility()方法时返回的ID,这个ID便是被填充的View的ID。在这里我们不需要操作MapView,只需要调用setVisibility(View.VISIBLE)方法即可。如果想要获取被填充的视图的引用,inflate()方法会直接返回该引用,不需要再次调用findViewById()方法。
使用ViewStub我们只需要改变其可视性就可以控制map的显示。


外链地址1

外链地址2

外链地址3

Hack 3 创建定制的ViewGroup

需求与使用:

假设我们需要实现一个扑克牌游戏的那种摆放布局,实现这种布局有什么方式呢?

1.我们可以直接在RelativeLayout布局管理器中,为其内部View控件指定margin属性值。

2.创建自定义ViewGroup。

方法2与方法1相比的优点是:

1.在不同Activity中复用该视图时,更易于维护。

2.开发者可以使用自定义属性来定制ViewGroup中子视图的位置。

3.布局文件更简明,更容易理解。

4.如果需要修改margin,不必重新手动计算每个子视图的margin。

这里我认为还有一个优点就是可以提高效率,如果别的地方还有需要这种布局的时候就可以直接使用。

3.1 理解Android绘制视图的方式

在开发文档中的介绍:

“绘制布局由两个遍历过程组成:测量过程和布局过程。测量过程由measure(int,int)方法完成,该方法从上到下遍历视图树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每个视图都保存了各自的尺寸信息。第二个过程由layout(int,int,int,int)方法完成,该方法也是由上而下遍历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所有子视图的位置信息。”

ViewGroup的绘制过程:

第一步是测量ViewGroup的宽度和高度,在onMeasure()方法中完成这步操作。在该方法中,ViewGroup通过遍历所有子视图计算出它的大小。最后一步操作,在onLayout()方法中完成,在该方法中,ViewGroup利用上一步计算出的测量信息,布局所有子视图。

3.2 创建CascadeLayout

自定义ViewGroup命名为CascadeLayout,使用的XML布局文件如下:

<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cascade="http://schemas.android.com/apk/res/com.manning.androidhacks.hack003"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.manning.androidhacks.hack003.view.CascadeLayout
android:layout_width="match_parent"
android:layout_height="match_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>


在xml中使用自定义属性时需要指定自定义命名空间

通过自定义的cascade就可以使用其自定义属性了

在xml中使用CascadeLayout布局需要指定完全限定类名

在res/values目录下创建属性文件attrs.xml,定义那些定制的属性:

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


这里需要指定水平间距和垂直间距的默认值,以便在未指定值时使用。把默认值保存在dimens.xml中:

<resources>
<dimen name="cascade_horizontal_spacing">10dp</dimen>
<dimen name="cascade_vertical_spacing">10dp</dimen>
</resources>


开始实现CascadeLayout,继承ViewGroup并重写构造函数、onMeasure()方法和onLayout()方法。

构造函数:

public class CascadeLayout extends ViewGroup{
private int mHorizontalSpacing;
private int mVerticalSpacing;
public CascadeLayout(Context context,AttributeSet attrs){
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();
}
}
...
}


当通过xml文件创建该视图的实例时会调用该构造函数

mHorizontalSpacing和mVerticalSpacing由自定义属性中获取,如果值未指定则使用默认值

在编写onMeasure()方法之前,先创建自定义LayoutParams类,该类用于保存每个子视图的x、y轴位置,我们把LayoutParams定义为CascadeLayout的内部类:

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

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


使用新定义的CascadeLayout.LayoutParams类,还需要重写CascadeLayout类中的其它一些方法。这些方法是checkLayoutParams(),generateDefaultLayoutParams(),generateLayoutParams(AttributeSetattrs)和generateLayoutParams(ViewGroup.LayoutParams p)。

onMeasure()方法编码:

@Override
protected void onMeasure(){
int width = 0;
int height = getPaddingTop();

final int count = getChildCount();
for(int i=0;i<count;i++){
View child = getChildAt(i);
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();
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());
}
}


该方法是以onMeasure()方法计算出的值为参数循环调用子View的layout()方法。

3.3 为子视图添加自定义属性

示例:添加特定子视图重写垂直间距方法可以使第一个子视图具有不同的垂直间距:

第一步是向attrs.xml文件中添加一个新的属性,如下:

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


因为属性名的前缀是layout_,没有包含一个视图属性,因此该属性会被添加到LayoutParams的属性表中。正如CascadeLayout类,在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是一个公共字段(field成员变量),我们会在CascadeLayout类的onMeasure()方法中使用该自段。如果子视图的LayoutParams包含verticalSpacing,就可以使用它。源码如下:

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


我们在开发过程中如果需要创建复杂布局时应该首先考虑使用自定义ViewGroup,这样虽然开始会增加工作量但之后就会省很多时间。在定制组件的同时允许开发者提供自定义行为和功能。

外链地址1

外链地址2

外链地址3

Hack 4 偏好设置使用技巧

Android SDK中提供了Preference(偏好设置)框架,利用这个开发者只需编辑一个简单的XML文件就能开发出一个易用的偏好设置界面。

处理这个的XML的Activity我们不能在继承android.app.Activity,而是继承android.preference.PreferenceActivity。

需要注意的地方:

偏好设置框架的目的是创建简单的偏好设置界面。如果开发者想添加更复杂的UI控件或逻辑,建议创建一个Activity并使用Dialog的主题,然后从偏好设置控件上启动它。

说实话这个我就没有使用过,感觉在大多数情况也不会用这个。所以这里我只是简单的介绍下,有兴趣的可以查看外链。

外链地址

第二章 添加悦目的动画效果

Hack 5 使用TextSwitcher和ImageSwitcher实现平滑过渡

介绍:

为了使过渡过程的视觉效果更自然,Android提供了TextSwitcher和ImageSwitcher这两个类分别替代TextView与ImageView。

TextSwitcher的简单的使用步骤:

1.通过findViewById()方法获取TextSwitcher对象的引用switcher,当然也可以直接在代码中构造该对象。

2.通过switcher.setFactory()方法指定TextSwitcher的ViewFactory。

3.通过switcher.setInAnimation()方法设置换入动画效果。

4.通过switcher.setOutAnimation()方法设置换出动画效果。

TextSwitcher的工作原理是:

首先通过ViewFactory创建两个用于在TextSwitcher中切换的视图,每当调用setText()方法时,TextSwitcher首先移除当前视图并显示setOutAnimation()方法设置的动画,然后并将另一个视图切换进来,并显示setInAnimation()方法设置的动画。

示例:实现文本切换的淡入淡出效果

//activity
private TextSwitcher mSwitcher;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Animation in = AnimationUtils.loadAnimation(this, android.R.anim.fade_in);
Animation out = AnimationUtils.loadAnimation(this, android.R.anim.fade_out);
mSwitcher = (TextSwitcher) findViewById(R.id.text_switch);
mSwitcher.setFactory(new ViewFactory() {

@Override
public View makeView() {
TextView textView = new TextView(MainActivity.this);
textView.setGravity(Gravity.CENTER);
textView.setText("hello");
return textView;
}
});

mSwitcher.setInAnimation(in);
mSwitcher.setOutAnimation(out);

mSwitcher.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mSwitcher.setText("world");
}
});
}
//xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >

<TextSwitcher
android:id="@+id/text_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</RelativeLayout>


TextSwitcher中的setCurrentText (CharSequence text)方法是设置当前显示的文本视图的文字内容。非动画方式显示。

ImageSwitcher与TextSwitcher的原理是一样的。使用ImageSwitcher与TextSwitcher可以让交互变的更加自然,但也要使用的位置得当,否则会适得其反。

外链地址1

外链地址2

Hack 6 为ViewGroup的子视图添加悦目的动画效果

Android提供了LayoutAnimationController类,用于为布局或ViewGroup的子视图添加动画效果。需要强调一点,开发者不可以为每个子视图分别指定不同的动画效果,但LayoutAnimationController可以决定各个子视图显示动画效果的时间。

示例:为ListView添加动画效果,效果是:每个子视图(也就是每个ListView的条目沿Y轴方向依次滑落到各自的位置上,同时由于子视图之间有一定延迟,所以整体看起来像瀑布一样)

//1.获取listview的引用
mListView = (ListView)findViewById(R.id.my_listview_id);
//2.创建默认动画集合对象
AnimationSet set = new AnimationSet(true);
//3.创建透明度渐变动画
Animation animation = new AlphaAnimation(0.0f,1.0f);
animation.setDuration(50);
set.addAnimation(animation);
//4.创建位移动画
animation = new TranslateAnimation(Animation.RELATIVE_TO_SELF,0.0f,Animation.RELATIVE_TO_SELF,0.0f,Animation.RELATIVE_TO_SELF,-1.0f,Animation.RELATIVE_TO_SELF,0.0f);
animation.setDuration(100);
set.addAnimation(animation);
//5.创建LayoutAnimationController对象并设置子视图动画效果持续时间
LayoutAnimationController controller = new LayoutAnimationController(set,0.5f);
//6.将LayoutAnimationController对象设置到listview中
mListView.setLayoutAnimation(controller);


2中传入AnimationSet构造函数的布尔型参数决定每个动画是不是使用同一个interpolator。

TranslateAnimation类构造函数定义如下:

/*
该接口很简单,需要传入x轴和y轴的起始与结束坐标。
Android提供三个选项,用于指定计算坐标参照:
Animation.ABSOLUTE
Animation.RELATIVE_TO_SELF
Animation.RELATIVE_TO_PARENT
*/
public TranslateAnimation(int fromXType,float fromXValue,int toXType,float toXValue,int fromYType,float fromYValue,int toYType,float toYValue){}


开发者可以将默认interpolator替换为BounceInterpolator。这样,当子视图滑落到预定义位置时,会出现弹跳效果。

外链地址1

外链地址2——Interpolator概念

Hack 7 在Canvas上显示动画

我们在为自定义UI控件添加动画效果时会发现动画相关的API是很有限的,所以Android提供了Canvas类满足这一需求。

Canvas类的开发文档说明:

“可以把Canvas视为Surface的替身或接口,图形便是绘制在Surface上的。Canvas封装了所有绘图调用。通过Canvas,绘制到Surface上的内容首先存储到与之关联到Bitmap中,该Bitmap最终会呈现到窗口上。 “

示例:绘制一个方块在屏幕上弹跳

Canvas类封装了所有绘图调用,我们可以创建一个View,重写其onDraw()方法,在该方法中便可以绘制基本的图形单元。

public class MainActivity extends Activity{
private DrawView mDrawView;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
//获取屏幕的宽和高
Display display = getWindowManager().getDefaultDisplay();
mDrawView = new DrawView(this);
mDrawView.height = display.getHeight();
mDrawView.width = display.getWidth();
//DrawView占据所有可用空间
setContentView(mDrawView);
}
}

//DrawView类实现
public class DrawView extends View{
private Rectangle mRectangle;
public int width;
public int height;
public DrawView(Context context){
super(context);
//创建方块对象
mRectangle = new Rectangle(context,this);
mRectangle.setARGB(255,255,0,0);
mRectangle.setSpeedX(3);
mRectangle.setSpeedY(3);
}
@Override
protected void onDraw(Canvas canvas){
//变换方块位置
mRectangle.move();
//将方块绘制到Canvas上
mRectangle.onDraw(canvas);
//重绘view
invalidate();
}
}


invalidate()这个方法可以强制重绘视图,在onDraw()方法中一般通过调用invalidate()方法变换视图的位置实现自定义动画。

外链地址1

外链地址2

Hack 8 附加Ken Burns特效的幻灯片

Ken Burns特效是视频产品中使用的一种平移和缩放静态图片的特效。

使用Nine Old Androids库可以让我们在旧版本上使用Android 3.0的动画API。

要创建Ken Burns特效,需要预设一些动画。这些动画将随机应用到ImageView,当一个动画显示完毕,就开始显示另一个动画和图片。

这个用的东西版本很老了,如果感兴趣可以看外链1里面的示例。

外链地址1

外链地址2

外链地址3

外链地址4

扩展

1.使用baselineAligned=”false”属性可以不会导致因为控件中内容长度的关系而导致的不对齐。值为false就不考虑控件中的内容进行对齐。

2.布局文件中我们在设置控件属性时,有的属性前面带layout,有的不带。它们的区别是什么呢?

带layout的属性在定义后都是交给这个控件的父控件进行分配的。

layout_gravity与gravity区别?

layout_gravity定义后相对于交给它的上层控件进行分配,它的分配会是这个控件在这个布局中的位置,而gravity分配的是这个控件里面内容的位置。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: