概念总结

为了应付Android设备各种尺寸的屏幕,为这些设备提供一个统一的解决方案,Android推出了Fragment组件。一种可以嵌入活动当中的UI片段。Android提供了位于两个不同包下的Fragment对象,android.app.Fragment支持Android4.0以上的版本,另一个包主要是兼容低版本的Android系统。

简单实现

要实现对Fragment的使用,首要需要和普通的UI布局一样声明该Fragment对应的布局,接着新建一个类继承自android.app.Fragment的自定义类,复写其中的onCreateView方法在其中通过LayoutInflater将刚才定义的left_fragment动态的加载进来。下面是一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00"
android:orientation="vertical" >


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="This is right fragment"
/>

</LinearLayout>

继承Fragment并复写其中onCreateView()

1
2
3
4
5
6
7
public class LeftFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.left_fragment, container, false);
return view;
}
}

在布局文件中,可以通过了fragment标签添加碎片,其中通过android:name来制定相应的属性来显式指明要添加的碎片类名

1
2
3
4
5
6
<fragment
android:id="@+id/left_fragment"
android:name="com.example.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />

动态的添加碎片

和上面的例子,首先需要声明碎片的布局文件,定义继承了Fragment的类并复写其中的onCreateView方法,在其中加载刚刚声明的布局文件。到这边都和全面一样,下面是在代码中实现动态添加碎片(碎片类名为AnotherRightFragment)

1
2
3
4
5
AnotherRightFragment fragment = new AnotherRightFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.commit();

可以将上面的步骤总结如下:
1.新建一个待添加的碎片实例
2.在活动类中使用getFragmentManager()方法获取fragmentManager对象实例
3.利用fragmentManager对象实例调用beginTransaction方法开启一个事务,传入要替换的组件id和待添加的碎片实例,进行替换操作
4.提交事务

碎片中的返回栈

为了像Activity一样利用backstack实现放回键退回到上一个界面,FragmentTransaction 中提供了一个addToBackStack()方法,可以用于将一个事务添加到返回栈中,只需要在transaction.replace方法后加入,transaction.addToBackStack(null);将这个事务加入到返回栈即可。其中传入的参数用于描述返回栈的名字。

碎片通信

在活动实现与碎片通信是通过FragmentManager的findFragmentById方法,从布局文件相应的组件中获取的碎片类对象。

1
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);

在碎片中实现与活动通信使用getActivity方法

1
MainActivity activity = (MainActivity) getActivity();

碎片之间的通信也是通过活动组件,首先在一个活动组件中活动与其相关的活动类,接着利用这个活动去获得另一个碎片即可。

碎片生命周期

首先onAttach()方法(当碎片和活动建立关联的时候调用)、onCreateView()方法(为碎片创建视图(加载布局)时调用)和onActivityCreated()方法(确保与碎片相关联的活动一定已经创建完毕的时候调用),回调完毕时碎片是可见的,他所相关的活动和其本身正处于运行状态。
碎片进入暂停状态是随着活动进如暂停转态,碎片也会进入暂停状态。
碎片进入停止状态的情况有,当活动进入停止状态时、调用了FragmentTransaction的remove和replace方法将碎片移除且在调用的addToBackStack方法。
碎片进入销毁状态的情况有,活动被销毁,调用了FragmentTransaction的remove和replace方法将碎片移除但没有在调用的addToBackStack方法,进入本状态依次回调onDestroyView()方法(当与碎片关联的视图被移除的时候调用)onDetach()方法(当碎片和活动解除关联的时候调用)。
将碎片的生命周期和活动一起考虑,各个函数回调的顺序见下图
image

使用限定符

一个最简单的实现就是将两个相同名称不同内容的视图文件,一个单页模式的用于适应手机屏幕的布局文件存放到layout/目录下,将一个双页模式的用于适应屏幕屏幕的布局文件存放到layout-large/目录下,这样android会自动判断什么屏幕是large,在大屏幕上采用layout-large/目录下的布局文件,下表就是android默认的一些限定符的含义。
image
自定义限定符,只需要新建相应的layout文件夹,如新建了layout-sw600dp/文件夹之后,当程序运行在屏幕宽度大于600dp 的设备上时就会加载layout-sw600dp/文件加下的文件

概念总结

Android中的广播分成标准广播(Normal broadcasts一直完全异步执行的广播所有接收器同时受到这个广播,没有先后顺序)和有序广播(Ordered broadcasts广播接收器按照优先级依次接收广播,优先级高的广播可以截断广播)。

广播接收器

广播接收类必须继承自BroadcastReceiver,并重写父类的onReceive()方法定义接收到广播之后的操作。
广播接收器有两种注册方式,在代码中动态的注册和在AndroidManifest.xml 中静态的注册。
下面是动态注册的基本例子,首先声明一个类继承制BroadcastReceiver,并复写其中的onReceive()方法

1
2
3
4
5
6
7
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "network changes",
Toast.LENGTH_SHORT).show();
}
}

接着在注册这个接收器的时候,首先通过IntentFilter定义需要接受的动作,新建这个广播接收器对象并将其注册

1
2
3
4
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(networkChangeReceiver, intentFilter);

最后在不需要这个广播接收器时,需要将其取消注册

1
unregisterReceiver(networkChangeReceiver);

下面是静态的注册广播接收器,非常简单,只需要在AndroidManifest.xml中声明一个接收器,给出其类名,以及需要接收的Intent

1
2
3
4
5
<receiver android:name=".BootCompleteReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

发送广播

标准广播

发送标准广播,只要在Intent对象中填入所需的广播名,在调用sendBroadcast()发送这个广播即可

1
2
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);

有序广播

和标准广播的区别就是发送广播时使用sendOrderedBroadcast()方法发送有序广播

1
2
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendOrderedBroadcast(intent, null);

其中第一个参数是Intent,第二个参数表示权限,在注册广播接收器的时候,可以通过android:priority 属性给广播接收器设置优先级

1
2
3
4
5
<receiver android:name=".MyBroadcastReceiver">
<intent-filter android:priority="100" >
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>

在接收到广播之后,可以在广播接收器中的onReceive()方法中使用abortBroadcast()方法将宝宝截断,后面优先级低于100的广播接收器就收不到该广播了。

本地广播

前面所说的广播都是属于系统广播,可以被其他程序截获,为了数据的安全性Android还有一套本地广播机制,即的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播
本地广播主要使用LocalBroadcastManager对广播进行管理,首先通过getInstance方法获取LocalBroadcastManager对象实例

1
localBroadcastManager = LocalBroadcastManager.getInstance(this);

在发送广播的时候,使用LocalBroadcastManager.sendBroadcast方法发送本地广播

1
2
Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);

在注册广播接收器时使用localBroadcastManager.registerReceiver方法注册广播接收器,广播接收器中onReceiver和非本地广播一样

1
2
3
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter);// 注册本地广播监听器

另外,本地广播接收器是无法进行静态注册的,因为静态注册就是为了程序在不运行的状态下也能接受广播,在本地广播的应用场景下不需要。

自定义的ListView

移动设备上的UI设计,用的最多的就是listView,从QQ、微信到一些新闻客户端,都是上下标题栏加上中间列表的结构,这样可以充分的利用移动设备并不富裕的空间,高效的显示更多的信息。Android中通过自定义的Adapter可以根据需要对ListView进行定制。
假设要在一个ListView中的每个条目展示不同的Fruit的名称和图片,首先新建一个Fruit类表示Fruit对象,其中name成员变量表示水果名,imageId表示对应的Fruit的图片资源id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}

public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}

接着需要定义一个对应于上面Fruit类的布局文件,作为ListView中每个条目的布局文件。下面就可以定义一个最基本的没有优化的适配器,这里是通过继承指定泛型为Fruit的ArrayAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;

public FruitAdapter(Context context, int itemViewResourceId,List<Fruit> objects) {
super(context, itemViewResourceId, objects);
resourceId = itemViewResourceId;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前项的Fruit实例
View view = LayoutInflater.from(getContext()).inflate(resourceId, null);
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}

可以看到,在构造函数中依次传入context对象,每个条目的布局文件的资源id,需要显示的所有Fruit对象组成的List列表。其实,实现自定义的适配器最主要的逻辑就在覆盖的getView方法中。这个方法在每个子项被滑动到屏幕内,即该子项将显示的时候被调用。可以看到这该方法中,我们首先调用gitItem方法根据位置position参数得到当前需要操作的fruit对象;接着,利用LayoutInflater来加载前面定义的好的子项布局;下面,通过通过findViewById方法将布局文件中各个组件view获取;最后获取fruit对象的数据,显示在布局文件相应的组件中,并返回这个组件即可。
但是,可以观察到大多数自定义的适配器中的getView方法都不是如此简单粗暴。上面的写法每次调用getview方法是都需要加载一次布局文件,这是多余了,完全可以利用convertView参数进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}

convertView参数用于将之前加载好的布局进行缓存,所以我们可以在加载布局文件之前进行判断,如果convertView不为空就不要再去利用LayoutInflater去加载布局。这里需要说的就是convertView,Android SDK中这样讲参数 convertview :
the old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using.
If it is not possible to convert this view to display the correct data, this method can create a new view.
为了节省内存,不至于让listview中的每个item都驻留在手机的内存中,Android提供了一套Recycler机制,当一个ListView初次新建时,getview中的convertView是null,这时用户开始滚动listview,当其中一个item被滚出屏幕后,android就将其保存在Recycler中而不是内存中。再次调用getview方法时,传入的convertView就是被滚出屏幕的item的布局了,只需要重新设定布局组件的数据即可。

下面,可以进一步优化,我们发现每次调用getView方法时,都会使用findViewById方法得到布局文件中每一个组件才能为其赋值。这也是十分影响效率的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder); // 将ViewHolder存储在View中
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag(); // 重新获取ViewHolder
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}

class ViewHolder {
ImageView fruitImage;
TextView fruitName;
}

通过定义内部类ViewHolder对布局文件的控件进行缓存,当convertView为空是我们创建一个viewHolder对象,将布局文件的组件都保存在其中。接着调用view的setTag方法将viewHolder对象保存在其中。在convertView不为空时,即可用从Recycler中取出旧的布局文件时,我们在通过view的getTag方法将布局文件的组件取出,以便接下来对其进行赋值。
这样,如果convertview是第一次展示我们就创建新的Holder对象与之绑定,并在最后通过return convertview返回,去显示;如果convertview 是回收来的那么我们就不必创建新的holder对象,只需要把原来的绑定的holder取出加上新的数据就行了。

制作Nine-Patch图片

有时候,我们需要制定一张图片那些部分是可以拉伸的,而不希望其均匀的被拉伸。这时候我们就可以利用android sdk中提供的一个工具,首先找到sdk根目录,打开其中的tools文件夹,运行draw9patch.bat,将要编辑的图片加载到其中。接着,在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分就表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分则表示内容会被放置的区域
image

1.使用Intent传递对象

典型的使用putExtra()以键值对的形式将数据放入Intent对象中

1
2
intent.putExtra("string_data", "hello");
intent.putExtra("int_data", 100);

在用相应的getXXXExtra()方法将数据取出

1
2
getIntent().getStringExtra("string_data");
getIntent().getIntExtra("int_data", 0);

但是getXXXExtra()方法中所支持的数据类型是有限的,比如自定义的对象存储在Intent就无法通过getXXXExtra()方法取出。这个时候就可以使用Serializable或Parcelable方式

Serializable序列化

在自定义的类中通过继承Serializable接口就可以将需要序列化的类,进行序列化,对这个类中不需要序列化的成员变量可以使用transient关键字对其进行声明。比如我们将自定义的实现了Serializable接口的类person对象存入Intent中

1
intent.putExtra("person_data", person);

取出的对象的时候就可以使用如下语句

1
Person person = (Person) getIntent().getSerializableExtra("person_data");

Parcelable方式

Parcelable 方式的实现方式与Serializable方式不同,它是通过将一个完整的对象分解成Intent支持的数据类型。
首先让自定义的类实现Parcelale接口,接着复写其中的describeContents()和writeToParcel()两个方法。在其中writeToParcel()方法中调用Parcel
对象的writeXxx()方法将自定义类中的成员变量一一写入。
最后,新建一个public static final Parcelable.Creator<自定义类> CREATOR常量,实现Parcelable.Creator<自定义类>接口,并将泛型制动成自定义的类,接着复写createFromParcel()和newArray()这两个方法,在createFromParcel()方法去读取刚才写出的自定义的类中成员变量
,并利用读取到的成员变量创建一个自定义对象进行返回。注意这里读取的顺序一定要和刚才写出的顺序完全相同。而newArray()方法中的实现就简单多了,只需要new 出一个自定义类数组,并使用方法中传入的size作为数组大小就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Person implements Parcelable {
private String name;
private int age;
……

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name); // 写出name
dest.writeInt(age); // 写出age
}

public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>() {
@Override
public Person createFromParcel(Parcel source) {
Person person = new Person();
person.name = source.readString(); // 读取name
person.age = source.readInt(); // 读取age
return person;
}

@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
}

相应的取出对象时使用语句

1
Person person = (Person) getIntent().getParcelableExtra("person_data");

2.自定制日志工具

为了在应用上线之后去掉打印日志,或者实现打印自定义级别的日志,如只打印Error级别日志信息,我们可以自定义一个日志类,对日志的信息进行管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LogUtil {
public static final int VERBOSE = 1;
public static final int DEBUG = 2;
public static final int INFO = 3;
public static final int WARN = 4;
public static final int ERROR = 5;
public static final int NOTHING = 6;
public static final int LEVEL = VERBOSE;
public static void v(String tag, String msg) {
if (LEVEL <= VERBOSE) {
Log.v(tag, msg);
}
}
public static void d(String tag, String msg) {
if (LEVEL <= DEBUG) {
Log.d(tag, msg);
}
}
public static void i(String tag, String msg) {
if (LEVEL <= INFO) {
Log.i(tag, msg);
}
}
public static void w(String tag, String msg) {
if (LEVEL <= WARN) {
Log.w(tag, msg);
}
}
public static void e(String tag, String msg) {
if (LEVEL <= ERROR) {
Log.e(tag, msg);
}
}
}

如上所示在LogUtil中可以通过调整Level的大小来自定义的打印的log的基本,Level为1表示打印全部log,为6表示所有级别都不打印,1到6打印的log的重要级别依次上升。

3.TDD在Android开发中应用

创建Android Test Project为相应的项目进行测试,生产的测试项目的AndroidManifest.xml文件中会指出该测试的目标项目,通过继承AndroidTestCase就可以编写测试类,在其中编写各种测试方法,具体的使用方法和java中junit的一样,可以看我github上AgileJavaNote这个项目。

最近在书上看到一些关于的Activity组件相关的技巧,觉得可以记录下来以后开发时进行实践。

显示活动名称

首先是实现进入一个活动就可以观察到自己进入的活动类的名称,这个非常简单但是在程序调试阶段比较有用,我们需要兴建一个类基础Activity类

1
2
3
4
5
6
7
public class BaseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("now in Activty: ", getClass().getSimpleName());
}
}

接着在项目中的每个活动类都继承子BaseActivity而不是Activity类,可以看到这里仅仅是在覆盖的onCreate方法利用java的放射机制打印出当前的类名。当然这里也存在一个问题,如果自定义的活动类不是继承自Activity而是其子类如FragmentActivity就不会打印出相应的类名

不使用活动栈,从任何一个活动退出

解决的思路很简单,新建一个活动在每个活动管理器类,用列表保存现在活动栈中所有的活动,当需要退出程序调用每个Activity对象的finish方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ActivityCollector {
public static List<Activity> activities = new ArrayList<Activity>();
public static void addActivity(Activity activity) {
activities.add(activity);
}
public static void removeActivity(Activity activity) {
activities.remove(activity);
}
public static void finishAll() {
for (Activity activity : activities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
}
}

下面在BaseActivity覆盖的onCreate方法中加入,将活动添加到ActivityCollector的成员变量中的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Log.d("now is Activity: ", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}

@Override
protected void onDestroy(){
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}

这是在任何一个活动中就可以调用ActivityCollector的finishAll方法直接退出应用了

间接的启动活动

传统的通过intent启动一个方法,在遇到需要启动其他人书写的活动时就不那么方便了,必须要阅读他人书写的活动源码,才能知道需要在intent中传递什么样的参数,通过下面的方法可以很轻易的告诉别人启动自己的活动需要哪些参数

1
2
3
4
5
6
7
8
9
public class testActivity extends BaseActivity {
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}
……
}

再启动testActivity时只要调用actionStart方法并按照方法中声明的参数传入相应的数据即可。