// 隐藏标题栏
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.first)layout);
}
Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间:
// 定义一个弹出Toast的触发点
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(r.layout.first_layout);
Button button1 = (Button) findViewById(R.id.button_1);
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(FirstActivity.this, "You clicked Button 1",
Toast.LENGTH_SHORT).show();
}
});
}
findViewById()方法返回的是一个View对象,我们需要向下转型将它转成Button对象。
makeText(0方法需要传入三个参数。第一个参数是Context,也就是Toast要求的上下文,由于活动本身就是一个Context对象,因此这里直接传入FirstActivity.this即可。第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG。
首先在res目录下新建一个menu文件夹,右击res目录->New->Folder,输入文件夹名menu,点击Finish。接着在这个文件夹下再新建一个名叫main的菜单文件,右击menu文件夹->New->Android XML File:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/add_item"
android:title="Add" />
<item
android:id="@+id/remove_item"
android:title="Remove" />
menu>
// 打开FirstActivity,重写onCreateOptionsMenu()方法
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
// 仅仅让菜单显示出来是不够的,还要再定义菜单响应事件,重写onOptionsItemSelected()方法
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.add_item:
Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
break;
case R.id.remove_item:
Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();
break;
default:
}
return true;
}
可以看到,菜单默认是不会显示出来的,只有按下了Menu键,菜单才会在底部显示出来,这样我们就可以放心地使用菜单了,因为它不会占用任何活动的空间。
只要按一下Back键就可以销毁当前的活动了。Activity类提供了一个finish()方法,我们在活动中调用一下这个方法就可以销毁当前活动了。
// 用intent启动Activity
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);
}
});
如果你想要回到上一个活动怎么办呢?很简单,按下Back键就可以销毁当前活动,从而回到上一个活动了。
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
<activity android:name=".SecondActivity" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
intent-filter>
activity>
只有
// 隐式的intent
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);
}
});
// 每个Intent中只能指定一个action,但却能指定多个category
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
intent.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(intent);
}
});
使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如说你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要调用系统的浏览器来打开这个网页就行了。
// intent启动浏览器
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
}
});
这里我们首先指定了Intent的action是Intent.ACTION_VIEW,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW。
与此对应,我们还可以在
android:scheme 用于指定数据的协议部分,如http
android:host 用于指定数据的主机名部分,如www.baidu.com
android:port 用于指定数据的端口部分,一般紧随在主机名之后
android:path 用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容
android:mimeType 用于指定可以处理的数据类型,允许使用通配符的方式进行指定
不过一般在标签中都不会指定过多的内容,如上面浏览器示例中,其实只需要指定android:scheme为http,就可以响应所有的http协议的Intent了。
<activity android:name=".ThirdActivity" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
intent-filter>
activity>
可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。点击Browser还会像之前一样打开浏览器,并显示百度的主页,而如果点击了ActivityTest,则会启动ThirdActivity。需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个活动并没有加载并显示网页的功能,所以在真正的项目中尽量不要去做这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。
除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。
// 在程序中调用系统拨号界面
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
});
// 用intent发送数据
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String data = "Hello SecondActivity";
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("extra_data", data);
startActivity(intent);
}
});
// 取出数据
public class SecondActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.second_layout);
Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
Log.d("SecondActivity", data);
}
}
这里由于我们传递的是字符串,所以使用getStringExtra()方法来获取传递的数据,如果传递的是整型数据,则使用getIntExtra()方法,传递的是布尔型数据,则使用getBooleanExtra()方法,以此类推。
Activity中还有一个startActivityForResult()方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。
// 使用startActivityForResult启动intent
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
}
});
// 添加返回数据
public class SecondActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.second_layout);
Button button2 = (Button) findViewById(R.id.button_2);
button2.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
setResult(RESULT_OK, intent);
finish();
}
});
}
}
// 由于我们是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁
// 之后会回调上一个活动的onActivityResult方法,因此我们需要在FirstActivity中重写这个方法来得到
// 返回的数据
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case 1:
if (resultCode==RESULT_OK) {
String returnedData = data.getStringExtra("data_return");
Log.d("FirstActivity", returnedData);
}
break;
default:
}
}
// 这时候你会问,如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity
// 这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,重写onBackPressed()
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
setResult(RESULT_OK, intent);
finish();
}
<activity android:name=".NormalActivity" >
activity>
<activity android:name=".DialogActivity" android:theme="@android:style/Theme.Dialog" >
activity>
由于NormalActivity已经把MainActivity完全遮挡住,因此onPause()和onStop()方法都会得到执行。
可以看到,只有onPause()方法得到了执行,onStop()方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。
想象以下场景,应用中有一个活动A,用户在活动A的基础上启动了活动B,活动A就进入了停止状态,这个时候由于系统内存不足,将活动A回收掉了,然后用户按下Back键返回活动A,会出现什么情况呢?其实还是会正常显示活动A的,只不过这时并不会执行onRestart()方法,而是会执行活动A的onCreate()方法。
Activity中还提供了一个onSaveInstanceState()回调方法,这个方法会保证一定在活动被回收之前调用,因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。
onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法来保存字符串,使用putInt()方法保存整型数据,以此类推。
// 保存临时数据到Bundle中
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}
数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是当活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可:
// 从Bundle中取出保存临时数据
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG, tempData);
}
...
}
Intent还可以结合Bundle一起用于传递数据的,首先可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。
// 新建一个ActivityCollector类作为活动管理器
public class ActivityCollector {
public static List activities = new ArrayList();
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中的代码
public class BaseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
// 从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了
public class ThirdActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("ThirdActivity", "Task id is " + getTaskId());
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.third_layout);
Button button3 = (Button) findViewById(R.id.button_3);
button3.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ActivityCollector.finishAll();
}
});
}
}
public class MainActivity extends Activity implements OnClickListener {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
// 在此处添加逻辑
break;
default:
break;
}
}
}
public class MainActivity extends Activity implements OnClickListener {
...
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
AlertDialog.Builder dialog =
new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("Something important.");
dialog.setCancelable(false);
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
});
dialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
});
dialog.show();
break;
default:
break;
}
}
}
public class MainActivity extends Activity implements OnClickListener {
...
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.button:
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();
break;
default:
break;
}
}
}
如果在setCancelable()中传入了false,表示ProgressDialog是不能通过Back键取消掉的,这时你就一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialog的dismiss()方法来关闭对话框,否则ProgressDialog将会一直存在。
布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 1" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 5" />
RelativeLayout>
注意,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TableRow>
<TextView
android:layout_height="wrap_content"
android:text="Account:" />
<EditText
android:id="@+id/account"
android:layout_height="wrap_content"
android:hint="Input your account" />
TableRow>
<TableRow>
<TextView
android:layout_height="wrap_content"
android:text="Password:" />
<EditText
android:id="@+id/password"
android:layout_height="wrap_content"
android:inputType="textPassword" />
TableRow>
<TableRow>
<Button
android:id="@+id/login"
android:layout_height="wrap_content"
android:layout_span="2"
android:text="Login" />
TableRow>
TableLayout>
在TableLayout中每加入一个TableRow就表示在表格中添加了一行,然后在TableRow中每加入一个控件,就表示在该行中加入了一列,TableRow中的控件是不能指定宽度的。
不过从图中可以看出,当前登录界面没有充分利用屏幕的宽度,右侧还空出了一块区域,这也难怪,因为在TableRow中我们无法指定空间的宽度。这时使用android:stretchColumns属性就可以很好地解决这个问题,它允许将TableLayout中的某一列进行拉伸,以达到自动适应屏幕宽度的作用。
这里将android:stretchColumns的值指定为1,表示如果表格不能完全占满屏幕宽度,就将第二列进行拉伸。没错!指定成1就是拉伸第二列,指定成0就是拉伸第一列。
可以看到,我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中一种最基本的UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础之上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多的子View和子ViewGroup,是一个用于放置控件和布局容器。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<include layout="@layout/title" />
LinearLayout>
// 新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件
public class TitleLayout extends LinearLayout {
public TtileLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 已经利用了前面写的title.xml
LayoutInflater.from(context).inflate(R.layout.title, this);
}
}
首先我们重写了LinearLayout中的带有两个参数的构造函数,在布局中引入TitleLayout控件就会调用这个构造函数。然后在构造函数中需要对标题栏布局进行动态加载,这就要借助LayoutInflater来实现了。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载的布局文件的id,这里我们传入R.layout.title,第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this。
<LinearLayout xmlns:android=... >
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>com.example.uicustomviews.TitleLayout>
LinearLayout>
添加自定义控件和添加普通控件的方式是一样的,只不过在添加自定义控件的时候我们需要指明控件的完整类名,包名在这里是不可以省略的。
重新运行程序,你会发现此时效果和使用引入布局方式的效果是一样的。
// 然后我们为标题栏中的按钮注册点击事件,修改TitleLayout中的代码
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
Button titleBack = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBack.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity) getContext()).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "You clicked Edit button",
Toast.LENGTH_SHORT).show();
}
});
}
}
这样的话,每当我们在一个布局中引入TitleLayout,返回按钮和编辑按钮的点击事件就已经自动实现好了,也是省去了很多编写重复代码的工作。
public class MainActivity extends Activity {
private String[] data = { "Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapter adapter = new ArrayAdapter(
MainActivity.this, android.R.layout.simple_list_item_1, data);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
不过,数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。
这里由于我们提供的数据都是字符串,因此将ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id,以及要适配的数据。注意我们使用了android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。
最后,还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成了。
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="10dip" />
LinearLayout>
// 接下来创建自定义的适配器
// 要知道其实适配器的处理单元好像是Item,并非整个ListView
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
// textViewResourceId指的是ListView子项布局的id
public FruitAdapter(Context context, int textViewResourceId,
List objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@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;
}
}
FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的id和数据都传递进来。另外又重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。在getView方法中,首先通过getItem()方法得到当前项的Fruit实例,然后使用LayoutInflater来为这个子项加载我们传入的布局,接着调用View的findViewById()方法分别获取到ImageView和TextView的实例,并分别调用它们的setImageResource()和setText()方法来设置显示的图片和文字,最后将布局返回,这样我们自定义的适配器就完成了。
public class MainActivity extends Activity {
private List fruitList = new ArrayList();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits(); //初始化水果数据
FruitAdapter adapter = new FruitAdapter(MainActivity.this,
R.layout.fruit_item, fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
private void initFruits () {
Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
fruitList.add(apple);
...
Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
fruitList.add(mango);
}
}
// 这样定制ListView界面的任务就完成了
虽然目前我们定制的界面还是很简单,但是相信你已经领悟到了诀窍,只要修改fruit_item.xml中的内容,就可以定制出各种复杂的界面了。
// 在适配器中应用这两个优化措施
public class FruitAdapter extends ArrayAdapter<Fruit> {
...
@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 fruitImage;
}
}
// 通过这两步优化之后,我们ListView的运行效率就已经非常不错了
// ListView响应用户的点击事件,修改MainActivity
public class MainActivity extends Activity {
private List fruitList = new ArrayList();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this,
R.layout.fruit_item, fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view,
int position, long id) {
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this, fruit.getName(),
Toast.LENGTH_SHORT).show();
}
});
}
...
}
可以看到,我们使用了setOnItemClickListener()方法来为ListView注册了一个监听器,当用户点击了ListView中的任何一个子项时就会回调onItemClick()方法,在这个方法中可以通过position参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toase将水果的名字显示出来。
// 我们可以通过代码来得知当前屏幕的密度值是多少
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;
Log.d("MainActivity", "xdpi is " + xdpi);
Log.d("MainActivity", "ydpi is " + ydpi);
}
}
根据Android的规定,在160dpi的屏幕上,1dp等于1px,而在320dpi的屏幕上,1dp就等于2px。因此,使用dp来指定控件的宽和高,就可以保证控件在不同密度的屏幕中的显示比例保持一致。
<LinearLayout ... >
<Button
android:id="@+id/button"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Button"
/>
LinearLayout>
sp的原理和dp是一样的,它主要是用于指定文字的大小,这里就不再进行介绍了。
总结一下,在编写Android程序的时候,尽量将控件或布局的大小指定成match_parent或wrap_content,如果必须要指定一个固定值,则使用dp来作为单位,指定文字大小的时候使用sp作为单位。
<RelativeLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/message_left" >
LinearLayout>
RelativeLayout>
可以看到,由于message_left的宽度不足以填满整个屏幕的宽度,整张图片被均匀地拉伸了!这种效果非常差,用户肯定是不能容忍的,这时我们就可以使用Nine-Patch图片来进行改善。
在Android sdk目录下有一个tools文件夹,在这个文件夹中找到draw9patch.bat文件,我们就是使用它来制作Nine-Patch图片的。双击打开之后,在导航栏点击File->Open 9-patch将message_left.png加载进来。
我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分就表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分则表示内容会被放置的区域。
这样当图片需要拉伸的时候,就可以只拉伸指定的区域,程序在外观上也是有了很大的改进。
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8"
android:orientation="vertical" >
<ListView
android:id="@+id/msg_list_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:divider="#0000" >
ListView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<EditText
android:id="@+id/input_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something here"
android:maxLines="2" />
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send" />
LinearLayout>
LinearLayout>
// 接着定义消息的实体类,新建Msg
public class Msg {
public static final int TYPE_RECEIVED = 0;
public static final int TYPE_SENT = 1;
private String content;
private int type;
public Msg(String content, int type) {
this.content = content;
this.type = type;
}
public String getContent() {
return content;
}
public int getType() {
return type;
}
}
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp" >
<LinearLayout
android:id="@+id/left_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="@drawable/message_left" >
<TextView
android:id="@+id/left_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff" />
LinearLayout>
<LinearLayout
android:id="@+id/right_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@drawable/message_right" >
<TextView
android:id="@+id/right_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp" />
LinearLayout>
LinearLayout>
你可能会有些疑虑,怎么能让收到的消息和发出的消息都放在同一个布局里呢?不用担心,可以利用可见属性,只要稍后在代码中根据消息的类型来决定隐藏和显示哪种消息就可以了。
// 接下来需要创建ListView的适配器类,让它继承ArrayAdapter,并将泛型指定为Msg类
public class MsgAdapter extends ArrayAdapter<Msg> {
// ViewHolder的域要包含ListView子项布局msg_item.xml中所有控件
class ViewHolder {
LinearLayout leftLayout;
LinearLayout rightLayout;
TextView leftMsg;
TextView rightMsg;
}
private int resourceId;
public MsgAdapter(Context context, int textViewResourceId, List objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Msg msg = getItem(position); // 这个方法太重要
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null); // 这个方法太重要
viewHolder = new ViewHolder();
viewHolder.leftLayout = (LinearLayout) view.findViewById(R.id.left_layout);
viewHolder.rightLayout = (LinearLayout) view.findViewById(R.id.right_layout);
viewHolder.leftMsg = (TextView) view.findViewById(R.id.left_msg);
viewHolder.rightMsg = (TextView) view.findViewById(R.id.right_msg);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
if (msg.getType() == Msg.TYPE_RECEIVED) {
// 如果是收到的消息,则显示左边的消息布局,将右边的消息布局隐藏
viewHolder.leftLayout.setVisibility(View.VISIBLE);
viewHolder.rightLayout.setVisibility(View.GONE);
viewHolder.leftMsg.setText(msg.getContent());
} else if(msg.getType() == Msg.TYPE_SENT) {
// 如果是发出的消息,则显示右边的消息布局,将左边的消息布局隐藏
viewHolder.rightLayout.setVisibility(View.VISIBLE);
viewHolder.leftLayout.setVisibility(View.GONE);
viewHolder.rightMsg.setText(msg.getContent());
}
return view;
}
}
// 最后修改MainActivity中的代码,来为ListView初始化一些数据,并给发送按钮加入事件响应
public class MainActivity extends Activity {
private ListView msgListView;
private EditText inputText;
private Button send;
private MsgAdapter adapter;
private List msgList = new ArrayList();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
initMsgs();
adapter = new MsgAdapter(MainActivity.this, R.layout.msg_item, msgList);
inputText = (EditText) findViewById(R.id.input_text);
send = (Button) findViewById(R.id.send);
msgListView = (ListView) findViewById(R.id.msg_list_view);
msgListView.setAdapter(adapter);
send.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String content = inputText.getText().toString();
if(!"".equals(content)) {
Msg msg = new Msg(content, Msg.TYPE_SENT);
msgList.add(msg);
adapter.notifyDataSetChanged(); // 当有新消息时,刷新ListView中的显示
// 调用ListView的setSelection()方法将显示的数据定位到最后一行,
// 以保证一定可以看到最后发出的一条消息
msgListView.setSelection(msgList.size());
inputText.setText("");
}
}
});
}
private void initMsgs() {
Msg msg1 = new Msg("Hello guy.", Msg.TYPE_RECEIVED);
msgList.add(msg1);
...
}
}
// LeftFragment类
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;
}
}
// RightFragment类与此相似
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent" >
<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
android:id="@+id/right_fragment"
android:name="com.example.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
LinearLayout>
可以看到,我们使用了
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent">
<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" />
<FrameLayout
android:id="@+id/right_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" >
<fragment
android:id="@+id/right_fragment"
android:name="com.example.fragmenttest.RightFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
FrameLayout>
LinearLayout>
FrameLayout是Android中最简单的一种布局,它没有任何定位方式,所有的控件都会摆放在布局的左上角。由于这里仅需要在布局里放入一个碎片,因此非常适合使用FrameLayout。
// 之后我们将在代码中替换FrameLayout中的内容,从而实现动态添加碎片的功能
public class MainActivity extends Activity implements OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.button:
// 动态添加碎片的五步
AnotherRightFragment fragment = new AnotherRightFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
tracsaction.commit();
break;
default:
break;
}
}
}
// FragmentTransaction中提供了一个addToBackStack()方法,可以用于将一个事务添加到返回栈中,
// 修改MainActivity中的代码
public class MainActivity extends Activity implements OnClickListener {
...
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
AnotherRightFragment fragment = new AnotherRightFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
break;
default:
break;
}
}
}
这里我们在事务提交之前调用了FragmentTransaction的addToBackStack()方法,它可以接收一个名字用于描述返回栈的状态,一般传入null即可。现在重新运行程序,并点击按钮将AnotherRightFragment添加到活动中,然后按下Back键,你会发现程序并没有退出,而是回到了RightFragment界面,再次按下Back键程序才会退出。
RightFragment rightFragment = (RightFragment) getFragmentManager()
.findFragmentById(R.id.right_fragment);
掌握了如何在活动中调用碎片的方法,那在碎片中又如何调用活动里的方法呢?其实这就更简单了,在每个碎片中都可以通过调用getActivity()方法来得到和当前碎片相关联的活动实例,代码如下所示:
MainActivity activity = (MainActivity) getActivity();
另外当碎片中需要使用Context对象时,也可以使用getActivity()方法,因为获取到的活动本身就是一个Context对象了。
大小:
small 提供给小屏幕设备的资源
normal 提供给中等屏幕设备的资源
large 提供给大屏幕设备的资源
xlarge 提供给超大屏幕设备的资源
分辨率:
ldpi 提供给低分辨率设备的资源(120dpi以下)
mdpi 提供给中等分辨率设备的资源(120dpi到160dpi)
hdpi 提供给高分辨率设备的资源(160dpi到240dpi)
xhdpi 提供给超高分辨率设备的资源(240dpi到320dpi)
方向:
land 提供给横屏设备的资源
port 提供给竖屏设备的资源
// 第一步准备好一个新闻的实体类,新建类News
public class News {
private String title;
private String content;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textSize="18sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
/>
LinearLayout>
android:padding表示给控件的周围加上补白,这样不至于让文本内容会紧靠在边缘上。android:singleLine设置为true表示让这个TextView只能单行显示。android:ellipsize用于设定当文本内容超出控件宽度时,文本的缩略方式,这里指定成end表示在尾部进行缩略。
// 第三步,创建新闻列表的适配器
public class NewsAdapter extends ArrayAdapter<News> {
private int resourceId;
public NewsAdapter(Context context, int textViewResourceId, List objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
News news = getItem(position); // 最重要的一个方法了,这个方法到底是何时使用的?数据是从哪儿来的?
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
newsTitleText.setText(news.getTitle());
return view;
}
}
// 在getView()方法中,我们获取到了相应位置上的News类,并让新闻的标题在列表中进行显示
<RelativeLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:id="@+id/visibility_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="invisible" >
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:textSize="20sp" />
<ImageView
android:layout_width="match_parent"
android:layout_height="1dp"
android:scaleType="fitXY"
android:src="@drawable/split_line" />
<TextView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="15dp"
android:textSize="18sp" />
LinearLayout>
<ImageView
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:scaleType="fitXY"
android:src="@drawable/split_line_vertical" />
RelativeLayout>
// 第五步,新建一个NewsContentFragment类,是新闻内容的Fragment
public class NewsContentFragment extends Fragment {
private View view;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
view = inflater.inflate(R.layout.news_content_frag, container, false);
return view;
}
public void refresh(String newsTitle, String newsContent) {
View visibilityLayout = view.findViewById(R.id.visibility_layout);
visibilityLayout.setVisibility(View.VISIBLE);
TextView newsTitleText = (TextView) view.findViewById(R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
newsTitleText.setText(newsTitle);
newsContentText.setText(newsContent);
}
}
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<fragment
android:id="@+id/news_content_fragment"
android:name="com.example.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
LinearLayout>
// 第七步,新建NewsContentActivity,作为显示新闻内容的活动
// 这个显然是在单页模式的时候使用的!因为这是个Activity啊!
public class NewsContentActivity extends Activity {
// 这个方法是static的,所以我们可以在程序的任何地方启动NewsContentActivity
// 同时,还告诉你的小伙伴,启动这个活动所需的参数
public static void actionStart(Context context, String newsTitle, String newsContent) {
Intent intent = new Intent(context, NewsContentActivity.class);
intent.putExtra("news_title", newsTitle);
intent.putExtra("news_content", newsContent);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 看,在单页模式里,加载的布局是news_content.xml
setContentView(R.layout.news_content);
String newsTitle = getIntent().getStringExtra("news_title");
String newsContent = getIntent().getStringExtra("news_content");
NewsContentFragment newsContentFragment = (NewsContentFragment)
getFragmentManager().findViewById(R.id.news_content_fragment);
newsContentFragment.refresh(newsTitle, newsContent);
}
}
<LinearLayout ...
android:layout_width="match_parent"
adnroid:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="@+id/news_title_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent" >
ListView>
LinearLayout>
// 第九步,新建NewsTitleFragment类,用来加载news_title_frag.xml布局
public class NewsTitleFragment extends Fragment implements OnItemClickListener {
private ListView newsTitleListView;
private List newsList;
private NewsAdapter adapter;
private boolean isTwoPane;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
newsList = getNews(); //初始化新闻数据
adapter = new NewsAdapter(activity, R.layout.news_item, newsList);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_title_frag, container, false);
newsTitleListView = (ListView) view.findViewById(R.id.news_title_list_view);
newsTitleListView.setAdapter(adapter);
newsTitleListView.setOnItemClickListener(this);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 在activity_main.xml里找news_content_layout这个控件,如果有,双页,没有,单页
if (getActivity().findViewById(R.id.news_content_layout) != null) {
isTwoPane = true;
} else {
isTwoPane = false;
}
}
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
News news = newsList.get(position);
if (isTwoPane) {
// 如果是双页模式,则刷新NewsContentFragment中的内容
NewsContentFragment newsContentFragment = (NewsContentFragment)
getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else {
// 如果是单页模式。则直接启动NewsContentActivity
NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
}
}
private List getNews() {
List newList = new ArrayList();
News news1 = new News();
news1.setTitle("...");
news1.setContent("......");
newsList.add(news1);
News news2 = new News();
news2.setTitle("...");
news2.setContent("......");
newsList.add(news2);
return newsList;
}
}
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:id="@+id/news_title_fragment"
android:name="com.example.fragmentbestpractice.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
LinearLayout>
<LinearLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:id="@+id/news_title_fragment"
android:name="com.example.fragmentbestpractice.NewsTitleFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/news_content_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
<fragment
android:id="@+id/news_content_fragment"
android:name="com.example.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
FrameLayout>
LinearLayout>
// 第十二步,将MainActivity稍作修改,把标题栏去除掉
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
}
}
// 通过动态注册的方式编写一个能够监听网络变化的程序
public class MainActivity extends Activity {
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();
}
}
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangeReceiver = new NetworkChangeReceiver();
registerReceiver(networkChangeReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangeReceiver);
}
}
当网络状态发生变化时,系统发出的正是一条值为android.net.conn.CONNECTIVITY_CHANGE的广播,也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的action就行了。
// 最好是能准确地告诉用户当前是有网络还是没有网络,我们再修改MainActivity
public class MainActivity extends Activity {
...
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectionManager = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
if (networkInfo!=null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable",
Toast.LENGTH_SHORT).show();
}
}
}
}
Android系统为了保证应用程序的安全性做了规定,如果程序需要访问一些系统的关键性信息,必须在配置文件中声明权限才可以,否则程序将会直接崩溃,比如这里查询系统的网络状态就是需要声明权限的。
<manifest ...
package="com.example.broadcasttest"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
...
manifest>
// 这里我们让程序接收一条开机广播,当收到这条广播时就可以在onReceive()方法里执行相应的逻辑
// 从而实现开机启动的功能!!!这也太牛了!
// 新建一个BootCompleteReceiver
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
}
}
// 这里我们不再使用内部类的方式定义广播接收器,因为我们需要在AndroidManifest.xml中将这个广播接收器的类名注册进去
<manifest ... >
...
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
<receiver android:name=".BootCompleteReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
intent-filter>
receiver>
application>
manifest>
首先打开到应用程序管理界面来查看一下当前程序所拥有的权限。在桌面按下Menu键->System settings->Apps,然后点击BroadcastTest。可以看到它目前拥有访问网络状态和开机自动启动的权限。
需要注意的是,不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会报错。因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动一个服务等。
// 点击按钮,发送自定义广播
public class MainActivity extends Activity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}
});
...
}
...
}
<manifest ... >
...
<application ...>
<receiver android:name=".MyBroadcastReceiver">
<intent-filter android:priority="100" >
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
intent-filter>
receiver>
application>
manifest>
// 既然已经获得了接收广播的优先权,那么MyBroadcastReceiver就可以选择是否允许广播继续传递了
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver",
Toast.LENGTH_SHORT).show();
abortBroadcast();
}
}
// 第二步,创建BaseActivity类作为所有活动的父类
public class BaseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
第三步,创建一个登陆界面的布局,我们在3.3.4节理已经编写过登陆界面了,新建布局文件login.xml。
// 第四步,登陆界面的布局已经完成,接下来就应该去编写登陆界面的活动,新建LoginActivity继承自BaseActivity
public class LoginActivity extends BaseActivity {
private EditText accountEdit;
private EditText passwordEdit;
private Button login;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login);
accountEdit = (EditText) findViewById(R.id.account);
passwordEdit = (EditText) findViewById(R.id.password);
login = (Button) findViewById(R.id.login);
login.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String account = accountEdit.getText().toString();
String password = passwordEdit.getText().toString();
if (account.equals("admin") && password.equals("123456")) {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
} else {
Toast.makeText(LoginActivity.this, "account or password is invalid",
Toast.LENGTH_SHORT).show();
}
}
});
}
}
第五步,你可以将MainActivity理解成是登陆成功后进入的程序主界面了,这里我们只需要加入强制下线功能就可以了,修改activity_main.xml,只有一个按钮。
// 第六步,修改MainActivity,在按钮事件里发送一条广播,通知程序强制用户下线
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button forceOffline = (Button) findViewById(R.id.force_offline);
forceOffline.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcastbestpractice.FORCE_OFFLINE");
sendBroadcast(intent);
}
});
}
}
也就是说强制用户下线的逻辑并不是写在MainActivity里的,而是应该写在接收这条广播的广播接收器里面,这样强制下线的功能就不会依附于任何的界面,不管是在程序的任何地方,只需要发出这样一条广播,就可以完成强制下线的操作了。
// 第七步,那么毫无疑问,我们需要创建一个广播接收器了,新建ForceOfflineReceiver
public class ForceOfflineReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);
dialogBuilder.setTitle("Warning");
dialogBuilder.setMessage("You are forced to be offline. Please try to login again.");
// 一定要调用setCancelable()方法将对话框设为不可取消,否则用户按一下Back键就可以关闭对话框继续使用程序了
dialogBuilder.setCancelable(false);
dialogBuilder.setPositiveButton("OK",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCollector.finishAll(); // 销毁所有活动
// 启动LoginActivity,回到Login界面
Intent intent = new Intent(context, LoginActivity.class);
// 由于是在广播接收器里启动活动的,因此一定要给Intent加入FLAG_ACTIVITY_NEW_TASK标志
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
});
AlertDialog alertDialog = dialogBuilder.create();
// 需要把对话框类型设为TYPE_SYSTEM_ALERT,不然它将无法在广播接收器里弹出
alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
alertDialog.show();
}
}
<manifest ...>
<uses-sdk ... />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application ...>
<activity
android:name=".LoginActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
<activity android:name=".MainActivity" >
activity>
<receiver android:name=".ForceOfflineReceiver" >
<intent-filter>
<action android:name="com.example.broadcastbestpractice.FORCE_OFFLINE" />
intent-filter>
receiver>
application>
manifest>
Git是一个开源的分布式版本控制工具,它的开发者就是鼎鼎大名的Linux操作系统的作者Linus Torvalds。Git被开发出来的初衷本是为了更好地管理Linux内核,而现在却早已被广泛应用于全球各种大中小型的项目中。
git config --global user.name "Thomas"
git config --global user.email "[email protected]"
配置完成后你还可以使用相同的命令来查看是否配置成功,只需要将最后的名字和邮箱地址去掉即可。
仓库创建完成后,会在BroadcastBestPractice项目的根目录下生成一个隐藏的.git文件夹,这个文件夹就是用来记录本地所有的Git操作的,可以通过ls -al命令来查看一下。
// 输出到文件
public void save() {
String data = "Data to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try {
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们可以借助DDMS的File Explorer来查看一下。切换到DDMS视图,并点击File Explorer切换卡,在这里进入到/data/data/com.example.filepersistencetest/files/目录下,可以看到生成了一个data文件。
// openFileInput()
public String load() {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while ((line = reader.readLine()) != null) {
content.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader!=null) {
try {
reader.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return content.toString();
}
我们在对字符串进行非空判断时使用了TextUtils.isEmpty()方法,这是一个非常好的方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要单独去判断这两种空值,再使用逻辑运算符连接起来了。
Context类中的getSharedPreferences()方法:此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/
/shared prefs/目录下的。MODE_MULTI_PROCESS则一般是用于会有多个进程中对同一个SharedPreferences文件进行读写的情况。
Activity类中的getPreferences()方法:这个方法和Context中的getSharedPreferences()方法相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。
PreferenceManager类中的getDefaultSharedPreferences()方法:这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。
// 修改MainActivity,向SharedPreferences文件存储数据
public class MainActivity extends Activity {
private Button saveData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
saveData = (Button) findViewById(R.id.save_data);
saveData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name", "Tom");
editor.putInt("age", 28);
editor.putBoolean("married", false);
editor.commit();
}
});
}
}
**SharedPreferences文件是使用XML格式来对数据进行管理的**:
<map>
<string name="name">Tomstring>
<int name="age" value="28" />
<boolean name="married" value="false" />
map>
SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
String name = pref.getString("name", "");
int age = pref.getInt("age", 0);
boolean married = pref.getBoolean("married", false);
<TableLayout ...
android:layout_width="match_parent"
android:layout_height="match_parent"
android:stretchColumns="1" >
...
<TableRow>
<CheckBox
android:id="@+id/remember_pass"
android:layout_height="wrap_content" />
<TextView
android:layout_height="wrap_content"
android:text="Remember password" />
TableRow>
<TableRow>
<Button
android:id="@+id/login"
android:layout_height="wrap_content"
android:layout_span="2"
android:text="Login" />
TableRow>
TableLayout>
// 定义MyDatabaseHelper,继承SQLiteOpenHelper
public class MyDatabaseHelper extends SQliteOpenHelper {
public static final String CREATE_BOOK = "create table Book ("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "pages integer, "
+ "name text)";
private Context mContext;
public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
// 创建数据库
public class MainActivity extends Activity {
private MyDatabaseHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dbHelper.getWritableDatabase();
}
});
}
}
第一次点击Create database按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就得到了创建,然后会弹出一个Toast提示创建成功。
如果还是使用File Explorer,那么最多你只能看到databases目录下出现了一个Bookstore.db文件,Book表是无法通过File Explorer看到的。因此这次我们使用adb shell来对数据库和表的创建情况进行检查。
adb是Android SDK中自带的一个调试工具,使用这个工具可以直接对连接在电脑上的手机或模拟器进行调试操作。它存放在sdk的platform-tools目录下,如果想要在命令行中使用这个工具,就需要先把它的路径配置到环境变量里。
如果你使用Windows,可以右击我的电脑->属性->高级->环境变量,然后在系统变量里找出Path并点击编辑,将platform-tools目录配置进去。
如果是Linux,可以在home路径下编辑.bash_profile文件,将platform-tools目录配置进去即可:
export PATH=$PATH:$HOME/android-sdk-linux/platform-tools
配置好环境变量后,就可以使用adb了。打开命令行界面,输入adb shell。然后cd进入/data/data/com.example.databasetest/databases/目录下,查看该目录里的文件。
借助sqlite命令来打开数据库,只需要键入sqlite3,后面加上数据库名即可。.table查看目前数据库中有哪些表。.schema查看它们的建表语句。.exit或.quit退出数据库的编辑。再exit就可以退出设备控制台。
public class MyDatabaseHelper extends SQLiteOpenHelper {
...
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("drop table if exists Book");
db.execSQL("drop table if exists Category");
onCreate(db);
}
}
(这里有个很重要的问题,什么叫重新运行,什么叫卸载?我觉得:重新运行就是重新run这个项目,重新编译。可能这样不同于卸载的地方,就是重新运行不会删掉已有的持久化存储,但是卸载就会。想想升级程序的时候的情景。)
这里先将已经存在的表删除掉,是因为如果在创建表时发现这张表已经存在了,就会直接报错。
接下来就是如何让onUpgrade()方法能够执行了,还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade()方法得到执行了。
这里将数据库版本号指定为2,表示我们对数据库进行升级了。现在重新运行程序,并点击Create database按钮,这时就会再次弹出创建成功的提示。
// 用SQLiteDatabase的insert()方法
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
// 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可
db.insert("Book", null, values);
values.clear();
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values);
// 用SQLiteDatabase的update()方法
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
// 第三个参数对应的是SQL语句的where部分,?是一个占位符,给第四个参数占的
db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" });
// 用SQLiteDatabase的delete()方法
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] { "500" });
query()方法的七个参数及对应SQL部分:
table - from table_name
columns - select column1, column2
selection - where column = value
selectionArgs - -
groupBy - group by column
having - having column = value
orderBy - order by column1, column2
调用query()方法后返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。
// 查询所有数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("pages"));
} while (cursor.moveToNext());
}
cursor.close();
添加数据:
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
new String[] { "The Da Vinci Code", "Dan Brown", "454", "16.96" });
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
new String[] { "The Lost Symbol", "Dan Brown", "510", "19.95" });
更新数据:
db.execSQL("update Book set price = ? where name = ?",
new String[] { "10.99", "The Da Vinci Code" });
删除数据:
db.execSQL("delete from Book where pages > ?", new String[] { "500" });
查询数据:
// 第二个参数null是什么意思,是说查询语句里没有占位符?所以不用填充?
db.rawQuery("select * from Book", null);
(虽然还是怪怪的,但就这样吧)
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
try {
db.delete("Book", null, null);
/* if (true) {
// 在这里手动抛出一个异常,让事务失败,那么上面的delete也会被回滚恢复
throw new NullPointerException();
} */
ContentValues values = new ContentValues();
values.put("name", "Game of Thrones");
values.put("author", "George Martin");
values.put("pages", 720);
values.put("price", 20.85);
db.insert("Book", null, values);
db.setTransactionSuccessful(); // 指commit?
} catch (Exception e) {
e.printStackTrace();
} finally {
db.endTransaction(); // 在MySQL里有start transaction和commit,除此之外就没了啊...
}
// 第一版本,只需要创建一张Book表,那MyDatabaseHelper中的onCreate和onUpgrade这样
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
}
@Override
public void onUpgrade(SQLiteDatabse db, int oldVersion, int newVersion) {
}
// 第二版,需要向数据库中再添加一张Category表,于是MyDatabaseHelper变成这样
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
default:
}
}
// 第三版,要给Book表和Category表之间建立关联,需要在Book表中添加一个category_id的字段
// 首先就是修改CREATE_BOOK这个String啦,添加一个category_id integer
// onCreate()方法不用变,新用户反正还是要建这两张表
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
case 2:
db.execSQL("alter table Book add column category_id integer");
default:
}
}
这里请注意一个非常重要的细节,switch中每一个case的最后都是没有使用break的,为什么要这么做呢?这是为了保证在跨版本升级的时候,每一次的数据库修改都能被全部执行到。比如用户当前是从第二版程序升级到第三版程序,那么case 2中的逻辑就会执行。而如果用户是直接从第一版程序升级到第三版程序的,那么case 1和case 2中的逻辑都会执行。使用这种方式来维护数据库的升级,不管版本怎样更新,都可以保证数据库的表结构是最新的,而且表中的数据也完全不会丢失了。
一些可以让其他程序进行二次开发的基础性数据,我们还是可以选择将其共享的。例如系统的电话簿程序,它的数据库中保存了很多的联系人信息,如果这些数据都不允许第三方的程序进行访问的话,恐怕很多应用的功能都要大打折扣了。除了电话簿之外,还有短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的技术当然就是内容提供器了。
content://com.example.app.provider/table1
content://com.example.app.provider/table2
Uri uri = Uri.parse("content://com.example.app.provider/table1");
getContentResolver().query()方法的参数及对应SQL部分
uri - from table_name
projection - select column1, column2
selection - where column = value
selectionArgs - 为where中的占位符提供具体的值
orderBy - order by column1, column2
getContentResolver().insert(uri, values);
getContentResolver().update(uri, values, "column1 = ? and column2 = ?",
new String[] {
"text", "1"});
getContentResolver().delete(uri, "column2= ?", new String[] { "1" });
// LinearLayout里就只放了一个ListView,修改MainActivity
public class MainActivity extends Activity {
ListView contactsView;
ArrayAdapter adapter;
List contactsList = new ArrayList();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList);
contactsView.setAdapter(adapter);
readContacts();
}
private void readContacts() {
Cursor cursor = null;
try {
cursor = getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null, null, null, null);
while (cursor.moveToNext()) {
String displayName = cursor.getString(cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
<manifest ...>
...
<uses-permission android:name="android.permission.READ_CONTACTS" />
...
manifest>
content://com.example.app.provider/table1/1
这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据。
内容URI的格式主要就只有两种,以路径结尾就表示期望访问该表中所有的数据,以id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配来匹配这两种格式的内容URI:
*:表示匹配任意长度的任意字符
#:表示匹配任意长度的数字
当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了:
// MyProvider中使用UriMatcher,分析URI,来判断该采取的动作
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);
uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_DIR);
uriMatcher.addURI("com.example.app.provider", "table2/#", TABLE2_ITEM);
}
...
@Override
public Cursor query(Uri uri, ...) {
switch (uirMatcher.match(uri)) {
case TABLE1_DIR:
// 查询table1表中的所有数据
break;
case TABLE1_ITEM:
// 查询table1表中的单条数据
break;
case TABLE2_DIR:
// 查询table2表中的所有数据
break;
case TABLE2_ITEM:
// 查询table2表中的单条数据
break;
default:
break;
}
...
}
...
一个内容URI所对应的MIME字符串主要由三部分组成,Android对这三个部分做了如下格式规定:
必须以vnd开头。
如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/。
最后接上vnd.. 。
所以对于content://com.example.app.provider/table1这个内容URI,它所对应的MIME类型可以写成:
vnd.android.cursor.dir/vnd.com.example.app.provider.table1
对于content://com.example.app.provider/table1/1这个内容URI,它所对应的MIME类型可以写成:
vnd.android.cursor.item/vnd.com.example.app.provider.table1
// 依照上面的,在MyProvider里实现getType()的逻辑,缺break吧??
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
default:
break;
}
return null;
}
因为所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行的,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
// 添加DatabaseProvider类
public class DatabaseProvider extends ContentProvider {
// 提供UriMatcher,为BOOK_DIR,BOOK_ITEM,CATEGORY_DIR,CATEGORY_ITEM添加URI
// 需要MyDatabaseHelper
private MyDatabaseHelper dbHelper;
// 重写boolean onCreate(), Cursor query(),
// 重写 Uri insert(), int update()
// 重写 int delete(), String getType()
Uri对象的getPathSegments()方法,它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id了。
还要在AndroidManifest.xml中注册内容提供器。
<provider
android:name="com.example.databasetest.DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:exported="true">
provider>
现在,就可以在ProviderTest程序的MainActivity里通过URI访问到DatabaseTest程序数据库中的表了。并继而可以增删改查。
bin/
gen/
那么当我们git add .时,bin目录和gen目录下的文件都会被忽略,不会被添加。
在项目根目录下输入:
git status
会提示哪些文件发生了更改。例如MainActivity.java发生过更改,要查看更改的内容:
git diff src/com/example/providertest/MainActivity.java
减号代表删除的部分,加号代表添加的部分。
例如,我修改了MainActivity.java,但还没有提交(还没有add),那么要撤销这个修改:
git checkout src/com/example/providertest/MainActivity.java
不过这种撤销方式只适用于那些还没有执行过add命令的文件,如果某个文件已经被添加过了,这种方式就无法撤销其更改的内容。
现在我们要先对齐取消添加,然后才可以撤回提交。取消添加使用reset命令:
git reset HEAD src/com/example/providertest/MainActivity.java
之后就能用checkout来撤销更改了。
git log
每次提交记录都会包含提交id,提交人,提交日期,以及提交描述(就是git commit -m 后面的字符串)这四个信息。
如果我们只想看一行记录,可以指定该记录的提交id,-l表示我们只想看到一行记录。如果要查看这条记录具体修改了什么内容,再加入-p参数。同样,减号表示删除的部分,加号代表添加的部分。
通知(Notification)是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通知后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。Android的通知功能获得了大量用户的认可和喜爱,就连iOS系统也在5.0版本之后加入了类似的功能。
// 在Activity里实现点击按钮发送通知
public class MainActivity extends Activity implements OnClickListener {
private Button sendNotice;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sendNotice = (Button) findViewById(R.id.send_notice);
sendNotice.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.send_notice:
// 创建通知的逻辑啦!
NotificationManager manager = (NotificationManager)
getSystemService(NOTIFICATION_SERVICE);
Notification notification = new Notification(R.drawable.ic_launcher,
"This is ticker text", System.currentTimeMillis());
notification.setLatestEventInfo(this, "This is content title",
"This is content text", null);
// 这里我们指定notification的id是1,后面cancel掉它的时候要用到
manager.notify(1, notification);
break;
default:
break;
}
}
}
如果你使用过Android手机,此时应该会下意识地认为这条通知是可以点击的。但是当你去点击它的时候,你会发现没有任何效果。其实想要实现通知的点击效果,我们还需要在代码中进行相应的设置,这就涉及到了一个新的概念,PendingIntent。
不同的是,Intent更加倾向于去立即执行某个动作,而PendingIntent则更加倾向于在某个合适的时机去执行某个动作。所以,也可以把PendingIntent简单地理解为延迟执行的Intent。
PendingIntent的用法同样很简单,它主要提供了几个静态方法用于获取PendingIntent的实例,可以根据需求来选择是使用getActivity()方法、getBroadcast()方法、还是getService()方法。第二个参数一般用不到,通常都是传入0即可。第三个参数是一个Intent对象,我们可以通过这个对象构建出PendingIntent的“意图”。第四个参数用于确定PendingIntent的行为,有FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT这四个值可选。
刚才我们将setLatestEventInfo()方法的第四个参数忽略掉了,直接传入了null,现在仔细观察一下,发现第四个参数正是一个PendingIntent对象。因此,这里就可以通过PendingIntent构建出一个延迟执行的“意图”,当用户点击这条通知时就会执行相应的逻辑。
// 给通知加入点击功能
public class MainActivity extends Activity implements OnClickListener {
...
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.send_notice:
NotificationManager manager = (NotificationManager)
getSystemService(NOTIFICATION_SERVICE);
Notification notification = new Notification(R.drawable.ic_launcher,
"This is ticker text", System.currentTimeMillis());
// 添加intent,点击通知后,启动NotificationActivity
Intent intent = new Intent(this, NotificationActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
notifcation.setLatestEventInfo(this, "This is content title",
"This is content text", pi);
manager.notify(1, notification);
break;
default:
break;
}
}
}
怎么系统状态上的通知图标还没消失呢?是这样的,如果我们没在代码中对该通知进行取消,它就会一直显示在系统的状态栏上。所以,我们在NotificationActivity中调用NotificationManager的cancel()方法:
public class NotificationActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.notification_layout);
NotificationManager manager = (NotificationManager)
getSystemService(NOTICATION_SERVICE);
// 1就是notification的id
manager.cancel(1);
}
}
// 音频Uri
Uri soundUri = Uri.fromFile(new File("/system/media/audio/ringtones/Basic_tone.ogg"));
notification.sound = soundUri;
除了允许播放音频外,我们还可以在通知到来的时候让手机进行振动,使用的是vibrate这个属性。它是一个长整型的数组,用于设置手机静止和振动的时长,以毫秒为单位。下标为0的值表示手机静止的时长,下标为1的值表示手机振动的时长,下标为2的值又表示手机静止的时长,以此类推。所以,如果想要让手机在通知到来的时候立刻振动1秒,然后静止1秒,再振动1秒,代码就可以写成:
// long[] vibrates = {0, 1000, 1000, 1000};
notification.vibrate = vibrates;
不过,想要控制手机振动还需要声明权限的:
<uses-permission android:name="android.permission.VIBRATE" />
现在的手机基本上都会前置一个LED灯,当有未接电话或未读短信,而此时手机又处于锁屏状态时,LED灯就会不同地闪烁,提醒用户去查看。我们可以使用ledARGB、ledOnMS、ledOffMS以及flags这几个属性来实现这种效果。ledARGB用于控制LED灯的颜色,一般有红绿蓝三种颜色可选。ledOnMS用于指定LED灯亮起的时长,以毫秒为单位。ledOffMS用于指定LED灯暗去的时长,也是以毫秒为单位。flags可用于指定通知的一些行为,其中就包括显示LED灯这一选项。所以,当通知到来时,如果想要实现LED灯以绿色的灯光一闪一闪的效果,就可以写成:
notification.ledARGB = color.GREEN;
notification.ledOnMS = 1000;
notification.ledOffMS = 1000;
notification.flags = Notification.FLAG_SHOW_LIGHTS;
当然,如果你不想进行这么多繁杂的设置,也可以直接使用通知的默认效果,它会根据当前手机的环境来决定播放什么铃声,以及如何振动:
notification.defaults = Notification.DEFAULT_ALL;
注意,以上所涉及的这些高级技巧都要在手机上运行才能看得到效果,模拟器是无法表现出振动、以及LED灯闪烁等功能的。
// 在MainActivity里不断完善功能
public class MainActivity extends Activity {
private TextView sender;
private TextView content;
private IntentFilter receiveFilter;
private MessageReceiver messageReceiver;
private IntentFilter sendFilter;
private SendStatusReceiver sendStatusReceiver;
private EditText to;
private EditText msgInput;
private Button send;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sender = (TextView) findViewById(R.id.sender);
content = (TextView) findViewById(R.id.content);
// 第二步,注册MessageReceiver,以及在onDestroy()里取消注册
receiveFilter = new IntentFilter();
receiveFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
messageReceiver = new MessageReceiver();
registerReceiver(messageReceiver, receiveFilter);
// 第四步,注册SendStatusReceiver,以及在onDestroy()里取消注册
sendFilter = new IntentFilter();
sendFilter.addAction("SENT_SMS_ACTION");
sendStatusReceiver = new SendStatusReceiver();
registerReceiver(sendStatusReceiver, sendFilter);
// 第三步,加入发送短信的处理逻辑
to = (EditText) findViewById(R.id.to);
msgInput = (EditText) findViewById(R.id.msg_input);
send = (Button) findViewById(R.id.send);
send.setOnClickListener(new OnClickListener(0 {
@Override
public void onClick(View v) {
SmsManager smsManager = SmsManager.getDefault();
// 第四步,利用senTextMessage()方法的第四个参数来对短信的发送状态进行监控
Intent sentIntent = new Intent("SENT_SMS_ACTION");
PendingIntent pi = PendingIntent.getBroadcast
(MainActivity.this, 0, sentIntent, 0);
// smsManager.sendTextMessage(to.getText().toString(), null,
msgInput.getText().toString(), null, null);
smsManager.sendTextMessage(to.getText().toString(), null,
msgInput.getText().toString(), pi, null);
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(messageReceiver);
unregisterReceiver(sendStatusReceiver);
}
// 第一步,创建内部类广播接收器MessageReceiver,处理收到短信之后的逻辑
class MessageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
Object[] pdus = (Object[]) bundle..get("pdus"); // 提取短信消息
SmsMessage[] messages = new SmsMessage[pdus.length];
for(int i=0; ibyte[]) pdus[i]);
String address = messages[0].getOriginatingAddress(); // 获取发送方号码
/* String fullMessage = "";
for(SmsMessage message : messages)
这么写显然不好,显然应该用StringBuilder
fullMessage += message.getMessageBody(); // 获取短信内容 */
StringBuilder sb = new StringBuilder();
for (SmsMessage message : messages)
sb.append(message.getMessageBody());
sender.setText(address);
sender.setText(sb.toString());
}
}
// 第四步,创建内部类广播接收器SendStatusReceiver来监控短信发送状态
class SendStatusReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (getResultCode() == RESULT_OK) {
// 短信发送成功
Toast.makeText(context, "Send succeeded", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(context, "Send failed", Toast.LENGTH_LONG).show();
}
}
}
}
第一步:首先我们从Intent参数中取出了一个Bundle对象,然后使用pdu密钥来提取一个SMS pdus数组,其中每一个pdu都表示一条短信消息。接着使用SmsMessage的createFromPdu()方法将每一个pdu字节数组转换为SmsMessage对象,调用这个对象的getOriginatingAddress()方法就可以获取到短信的发送方号码,调用getMessageBody()方法就可以获取到短信的内容,然后将每一个SmsMessage对象中的短信内容拼接起来,就组成了一条完成的短信。(意思是一条短信呗分割成了多个SmsMessage?)
第二步:还需要给程序声明一个接收短信的权限:
<uses-permission android:name="android.permission.RECEIVE_SMS" />
我们使用的是模拟器,模拟器上怎么可能会收得到短信呢?不用担心,DDMS提供了非常充分的模拟环境,使得我们不需要支付真正的短信费用也可以模拟收发短信的场景。将Eclipse切换到DDMS视图下,然后点击Emulator Control切换卡,在这里就可以向模拟器发送短信了。
在activity_main.xml里我们又新增了两个LinearLayout,分别处于第三行和第四行的位置。第三行中放置了一个EditText,用于输入接收方的手机号。第四行中放置了一个EditText和一个Button,用于输入短信内容和发送短信。
第三步,发送短信也是需要声明权限的:
<uses-permission android:name="android.permission.SEND_SMS" />
第四步,不过点击Send按钮虽然可以将短信发送出去,但是我们不知道到底发送成功了没有,这个时候就可以利用sendTextMessage()方法的第四个参数来对短信的发送状态进行监控。
<LinearLayout ...>
<Button
android:id="@+id/take_photo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Take Photo" />
<ImageView
android:id="@+id/picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
LinearLayout>
public class MainActivity extends Activity {
// 第二步,实现调用摄像头的具体逻辑
public static final int TAKE_PHOTO = 1;
public static final int CROP_PHOTO = 2;
private Button takePhoto;
private ImageView picture;
private Uri imageUri;
public static final int CHOOSE_PHOTO = 3;
private Button chooseFromAlbum;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
takePhoto = (Button) findViewById(R.id.take_photo);
picture = (ImageView) findViewById(R.id.picture);
takePhoto.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 创建File对象,用于存储拍照后的图片
File outputImage = new File(Environment.getExternalStorageDirectory(),
"output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
imageUri = Uri.fromFile(outputImage);
// 使用隐式intent,相机程序会响应这个intent的
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// TAKE_PHOTO作为onActivityResult()的requestCode
startActivityForResult(intent, TAKE_PHOTO); // 启动相机程序
}
});
// 第三步,点击chooseFromAlbum按钮,从相册选择照片的逻辑
chooseFromAlbum = (Button) findViewById(choose_from_album);
chooseFromAlbum.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 相册程序会响应这个intent的
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
Intent intent = new Intent("com.android.camera.action.CROP");
// 这句是将imageUri置为"image/*"吧
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, CROP_PHOTO); // 启动裁剪程序
}
break;
case CROP_PHOTO:
if (resultCode == RESULT_OK) {
try {
// 用到了getContentResolver(),说明image目录下的图片都是系统的ContentProvider吧
Bitmap bitmap = BitmapFactory.decodeStream(
getContentResolver().openInputStream(ImageUri));
picture.setImageBitmap(bitmap); // 显示剪裁后的图片
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
break;
// 第三步,打开相册之后的处理逻辑
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK) {
// 判断手机系统版本号
if (Build.VERSION.SDK_INIT >= 19) {
// 4.4及以上系统使用这个方法处理图片
handleImageOnKitKat(data);
} else {
// 4.4以下系统使用这个方法处理图片
handleImageBeforeKitKat(data);
}
}
break;
default:
break;
}
}
@TargetApi(19)
private void handleImageOnKitKat(Intent data) {
String imagePath = null;
Uri uri = data.getData();
if (DocumentsContract.isDocumentUri(this, uri)) {
// 如果是document类型的Uri,则通过document id处理
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthority())) {
String id = docId.split(":")[1]; // 解析出数字格式的id
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
} else if ("com.android.providers.downloads.documents".equals(
uri.getAuthority())) {
Uri contenUri = ContentUri.withAppendedId(Uri.parse(
"content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// 如果不是document类型的Uri,则使用普通方式处理
imagePath = getImagePath(uri, null);
}
displayImage(imagePath);
}
private void handleImageBeforeKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null);
displayImage(imagePath);
}
private String getImagePath(Uri uri, String selection) {
String path = null;
// 通过Uri和selection来获取真实的图片路径
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if (cursor!=null) {
if(cursor.moveToFirst()) {
path = cursor.getString(cursor.getColumnIndex(Media.DATA));
}
cursor.close();
}
return path;
}
private void displayImage(String imagePath) {
if (imagePath!=null) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
picture.setImageBitmap(bitmap);
} else {
Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
}
}
}
第二步:首先这里创建了一个File对象,用于存储摄像头拍下的图片,这里我们把图片命名为output_image.jpg,并将它存放在手机SD卡的根目录下,调用Environment的getExternalStorageDirectory()方法获取的就是手机SD卡的根目录。
接着构建出一个Intent对象,并将这个Intent的action指定为android.media.action.IMAGE_CAPTURE,再调用Intent的putExtra()方法指定图片的输出地址,这里填入刚刚得到的Uri对象,最后调用startActivityForResult()来启动活动。由于我们使用的是一个隐式Intent,系统会找出能够响应这个Intent的活动去启动,这样照相机程序就会被打开,拍下的照片将会输出到output_image.jpg中。
我们是使用startActivityForResult()来启动活动的,因此拍完照后会在结果返回到onActivityResult()方法中。如果发现拍照成功,则会再次构建出一个Intent对象,并把它的action指定为com.android.camera.action.CROP。这个Intent是用于对拍出的照片进行剪裁的,因为摄像头拍出的照片都比较大,而我们可能只希望截取其中的一小部分。然后给这个Intent设置一些必要的属性,并再次调用startActivityForResult()来启动剪裁程序。裁剪后的照片同样会输出到output_image.jpg中。
裁剪操作完成后,程序又会回调到onActivityResult()方法中,这个时候我们就可以调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它设置到ImageView中显示出来。
向SD卡中写数据需要申明权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
点击Take Photo按钮就可以进行拍照,拍照完成后点击确定则可以对照片进行剪裁,点击完成,就回到我们程序的界面,同时,裁剪后的照片当然也会显示出来。
private void initMediaPlayer() {
try {
File file = new File(Environment.getExternalStorageDirectory(), "music.mp3");
mediaPlayer.setDataSource(file.getPath());
mediaPlayer.prepare();
} catch ...
}
mediaPlayer.start();
mediaPlayer.pause();
mediaPlayer.reset(); //停止播放
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
}
}
private void initVedioPath() {
File file = new File(Environment.getExternalStorageDirectory(), "movie.3gp");
videoView.setVideoPath(file.getPath());
}
videoView.start();
videoView.pause();
videoView.resume(); // 重新播放
@Override
protected void onDestroy() {
super.onDestroy();
if(videoView!=null)
videoView.suspend();
}
其实VideoView只是帮我们做了一个很好的封装而已,它的背后仍然是使用MediaPlayer来对视频文件进行控制的。另外需要注意,VideoView并不是一个万能的视频播放工具类,它在视频格式的支持以及播放效率方面都存在着较大的不足。
在这三大智能手机操作系统中,iOS是不支持后台的,当应用程序不在前台运行时就会进入到挂起状态。Android则是沿用了Symbian的老习惯,加入了后台功能,这使得应用程序即使在关闭的情况下仍然可以在后台继续运行。而Windows Phone则是经历了一个由不支持到支持后台的过程,目前Windows Phone 8系统也是具备后台功能的。
new Thread(new Runnabl() {
@Override
public void run() {
// 处理逻辑
}
}).start();
// 使用Android异步消息处理机制,在子线程构造Message并用handler发送,在主线程用Handler处理Message并更改UI
public class MainActivity extends Activity implements OnClickListener {
public static final int UPDATE_TEXT = 1;
private TextView text;
private Button changeText;
// 主线程构造Handler
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch(msg.what) {
case UPDATE_TEXT:
// 在这里可以进行UI操作,因为这是在主线程
text.setText("Nice to meet you");
break;
default:
break;
}
}
};
...
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
// 在子线程创建并发送消息
Message message = new Message();
message.what = UPDATE_TEXT;
// 发送message是handler的方法,这样也就可以顺便决定哪个handler可以处理这个消息了
handler.sendMessage(message);
}
}).start();
break;
default:
break;
}
}
}
class DownloadTask extends AsyncTask {
...
}
AsyncTask类需要经常重写的几个方法:onPreExecute();doInBackground(Params…),这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。注意,这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行速度,可以调用publishMessage(Progress…)方法来完成;onProgressUpdate(Progress…),当在后台任务中调用了publishProgress(Progress…)方法后,这个方法就会很快被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值可以对界面元素进行相应的更新;onPostExecute(Result)。
// 一个比较完整的自定义AsyncTask
class DownloadTask extends AsyncTask {
@Override
protected void onPreExecute() {
progressDialog.show(); // 显示进度对话框
}
@Override
protected Boolean doInBackground(Void... params) {
try {
while(true) {
int downloadPercent = doDownload(); // 这是一个虚构的方法
publishProgress(downloadPercent);
if (downloadPercent>=100)
break;
}
} catch (Exception e) {
return false;
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
// 在这里更新下载进度
progressDialog.setMessage("Download " + values[0] + "%");
}
@Override
// 这个result就是doInBackground返回的
protected void onPostExecute(Boolean result) {
progressDialog.dismiss(); // 关闭进度对话框
}
}
要启动这个AsyncTask,只要
new DownloadTask().execute();
<service android:name=".MyService" >
service>
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent);
break;
default:
break;
}
}
// 在Service中自定义Binder,Binder里的方法可以在Activity里使用
public class MyService extends Service {
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {
public void startDownload() {
Log.d("MyService", "startDownload executed");
}
public int getProgress() {
Log.d("MyService", "getProgress executed");
return 0;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
...
}
下面就要看一看,在活动中如何去调用服务里的这些方法了。
这两个按钮分别是用于绑定服务和取消绑定服务的,那到底谁需要去和服务绑定呢?当然就是活动了。当一个活动和服务绑定了之后,就可以调用该服务里的Binder提供的方法了。修改MainActivity:
// 在Activity中绑定服务,然后就能调用服务里的Binder提供的方法了
public class MainActivity extends Activity implements OnClickListener {
...
private Button bindService;
private Button unbindService;
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
};
...
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务
break;
case R.id.unbind_service:
unbindService(connection); // 解绑服务
break;
...
}
}
}
我们首先创建了一个ServiceConnection的匿名类,在里面重写了onServiceConnected()方法和onServiceDisconnected()方法,这两个方法分别会在活动与服务成功绑定及解除绑定的时候调用。在onServiceConnected()方法里,我们又通过向下转型得到了DownloadBinder的实例,有了这个实例,活动和服务之间的关系就变得非常紧密了。现在我们可以在活动中根据具体的场景来调用DownloadBinder中的任何public方法,即实现了指挥服务干什么,服务就去干什么的功能。
bindService()方法接收三个参数,第一个参数就是刚刚创建出的Intent对象,第二个参数是ServiceConnection的实例,第三个参数是一个标志位,这里传入BIND_AUTO_CREATE表示在活动和服务进行绑定后自动创建服务。这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行。
另外需要注意,任何一个服务在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity绑定,还可以和任何一个其他的活动进行绑定,而且在绑定完成后它们都可以获取到相同的DownloadBinder实例。
public class MyService extends Service {
...
@Override
protected void onCreate() {
super.onCreate();
Notification notification = new Notification(R.drawable.ic_launcher,
"Notification comes", System.currentTimeMillis());
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
notificationIntent, 0);
notification.setLatestEventInfo(this, "This is title", "This is content",
pendingIntent);
startForeground(1, notification);
Log.d("MyService", "onCreate executed");
}
...
}
只不过这次在构建出Notification对象后并没有使用NotificationManager来将通知显示出来,而是调用了startForeground()方法。这个方法接收两个参数,第一个参数是通知的id,类似于notify()方法的第一个参数,第二个参数是构建出的Notification对象。调用startForeground()方法后就会让MyService变成一个前台服务,并在系统状态栏显示出来。
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService"); // 调用父类的有参构造函数
}
@Override
protected void onHandleIntent(Intent intent) {
// 打印当前线程的id
Log.d("MyIntentService", "Thread id is " + Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService", "onDestroy executed");
}
}
然后要在子类中去实现onHandleIntent()这个抽象方法,在这个方法中可以去处理一些具体的逻辑,而且不用担心ANR问题,因为这个方法已经是在子线程中运行的了。这里为了证实一下,我们在onHandleIntent()方法中打印了当前线程的id。另外根据IntentService的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了onDestroy()方法,在这里也打印了一行日志,以证实服务是不是停止掉了。
你会发现,其实IntentService的用法和普通的服务没什么两样。
long triggerAtTime = SystemClock.elapsedRealtime() + 10*1000;
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);
第一个参数是一个整型参数,用于指定AlarmManager的工作类型,有四种值可选,分别是ELAPSED_REALTIME、ELAPSED_REALTIME_WAKEUP、RTC和RTC_WAKEUP。其中ELAPSED_REALTIME表示让定时任务的触发时间从系统开机算起,但不会唤醒CPU。ELAPSED_REALTIME_WAKEUP同样表示让定时任务的触发时间从系统开机开始算起,但会唤醒CPU。RTC表示让定时任务的触发时间从1970年1月1日0点开始算起,但不会唤醒CPU。RTC_WAKEUP同样表示让定时任务的触发时间从1970年1月1日0点开始算起,但会唤醒CPU。使用SytemClock.elapsedRealtime()方法可以获取到系统开机至今所经历时间的毫秒数,使用System.currentTimeMillis()方法可以获取到1970年1月1日0点至今所经历时间的毫秒数。
第三个参数是一个PendingIntent,这里我们一般会调用getBroadcast()方法来获取一个能够执行广播的PendingIntent。这样,当定时任务被触发的时候,广播接收器的onReceive()方法就可以得到执行。(PendingIntent其实就是到点了要去启动的活动啦,广播接收器啦,服务啦)
// 创建一个可以长期在后台执行定时任务的服务。第一步,新增一个LongRunningService
public class LongRunningService extends Service {
... // onBind()
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
Log.d("LongRunningService", "executed at " + new Date().toString());
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int anHour = 60*60*1000;
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
// PendingIntent包含一个启动AlarmReceiver的intent
Intent i = new Intent(this, AlarmReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, 0);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
return super.onStartCommand(intent, flags, startId);
}
}
// 第二步,新建AlarmReceiver
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 又回头启动LongRunningService,循环了
Intent i = new Intent(context, LongRunningService.class);
context.startService(i);
}
}
onReceive()方法里的代码非常简单,就是构建出一个Intent对象,然后去启动LongRunningService这个服务。为什么这样写?其实在不知不觉中,这就已经将一个长期在后台定时运行的服务完成了。因为一旦启动LongRunningService,就会在onStartCommand()方法里设定一个定时任务,这样一小时后AlarmReceiver的onReceive()方法就将得到执行,然后我们在这里再次启动LongRunningService,这样就形成了一个永久的循环,保证LongRunningService可以每隔一小时就会启动一次,一个长期在后台定时运行的服务自然也就完成了。
// 第三步,在MainActivity打开程序的时候启动一次LongRunningService,之后LongRunningService就可以一直运行了
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, LongRunningService.class);
startService(intent);
}
}
第四步,注册服务和广播接收器。
从Android 4.4版本开始,系统会自动检测目前有多少Alarm任务存在,然后将触发时间相近的几个任务放在一起执行,这就可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的使用时间。
使用AlarmManager的setExact()方法来替代set()方法,可以保证任务准时执行。
<LinearLayout ...>
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
LinearLayout>
public class MainActivity extends Activity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
webView = (WebView) findViewById(R.id.web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("http://www.baidu.com");
}
}
我们调用了WebView的setWebViewClient()方法,并传入了一个WebViewClient的实例。这段代码的作用是,当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前的WebView中显示,而不是打开系统浏览器。
访问网络也是需要声明权限的:
<uses-permission android:name="android.permission.INTERNET" />
public class MainActivity extends Activity implements OnClickListener {
public static final int SHOW_RESPONSE = 0;
...
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch(msg.what) {
case SHOW_RESPONSE:
String response = (String) msg.obj;
responseText.setText(response);
}
}
};
...
@Override
public void onClick(View v) {
if (v.getId() == R.id.send_request) {
sendRequestWithHttpURLConnection();
}
}
private void sendRequestWithHttpURLConnection() {
// 开启线程来发起网络请求
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL("http://www.baidu.com");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while (line==reader.readLine())!=null)
response.append(line);
Message message = new Message();
message.what = SHOW_RESPONSE;
message.obj = response.toString();
handler.sendMessage(message);
} catch ...
} finally {
if (connection!=null)
connection.disconnect();
}
}
}).start();
}
}
那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将HTTP请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键值对的形式存在,数据与数据之间用&符号隔开,比如说我们想要向服务器提交用户名和密码,就可以这样写:
connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
httpClient.execute(httpGet);
然后通过一个NameValuePair集合来存放待提交的参数,并将这个参数集合传入到一个UrlEncodedFormEntity中,然后调用HttpPost的setEntity()方法将构建好的UrlEncodedFormEntity传入:
HttpPost httpPost = new HttpPost("http://www.baidu.com");
List params = new ArrayList();
params.add(new BasicNameValuePair("username", "admin"));
params.add(new BasicNameValuePair("password", "123456"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "utf-8");
httpPost.setEntity(entity);
httpClient.execute(httpPost);
执行execute()方法后会返回一个HttpResponse对象,服务器所返回的所有信息就会包含在这里面。
if (httpResponse.getStatusLine().getStatusCode() == 200) {
// 请求和响应都成功了
}
HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity);
// 有中文
String response = EntityUtils.toString(entity, "utf-8");
private void sendRequestWithHttpClient() {
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
HttpResponse httpResponse = httpClient.execute(httpGet);
if (httpResponse.getStatusLine().getStatusCode() == 200) {
// 请求和响应都成功了
HttpEntity entity = httpReponse.getEntity();
String response = EntityUtils.toString(entity, "utf-8");
Message message = new Message();
message.what = SHOW_RESPONSE;
message.obj = response.toString();
handler.sendMessage(message);
}
} catch ...
}
}).start();
}
<apps>
<app>
<id>1id>
<name>Google Mapsname>
<version>1.0version>
app>
<app>
<id>2id>
<name>Chromename>
<version>2.1version>
app>
<app>
<id>3id>
<name>Google Playname>
<version>2.3version>
app>
// xmlData是用httpGet得来的response
private void parseXMLWithPull(String xmlData) {
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
xmlPullParser.setInput(new StringReader(xmlData));
// getEventType()得到当前的解析事件
int eventType = xmlPullParser.getEventType();
String id = "";
String name = "";
String version = "";
while (eventType != XmlPullParser.END_DOCUMENT) {
// getName()获得当前节点的名字,可能是"id","name","version"
String nodeName = xmlPullParser.getName();
switch (eventType) {
// 开始解析某个结点
case XmlPullParser.SMART_TAG: {
if ("id".equals(nodeName)) {
// nextText()获得具体内容
id = xmlPullParser.nextText();
} else if ("name".equals(nodeName)) {
name = xmlPullParser.nextText();
} else if ("version".equals(nodeName)) {
version = xmlPullParser.nextText());
}
break;
}
// 完成解析某个结点
case XmlPullParser.END_TAG: {
if ("app".equals(nodeName)) {
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
break;
}
default:
break;
}
eventType = xmlPullParser.next();
}
} catch ...
}
// 新建ContentHandler继承自DefaultHandler,重写五个方法,解析逻辑全在里边
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
@Override
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();
version = new StringBuilder();
}
@Override
// 当遇到例如时调用,这时localName就被置为"app"
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
// 记录当前结点名
nodeName = localName;
}
@Override
// 遇到数据部分就被触发,这时的nodeName就是startElement时候的localName
public void characters(char[] ch, int start, int length) throws SAXException {
// 根据当前的结点判断将内容添加到哪一个StringBuilder对象中
if ("id".equals(nodeName)) {
id.append(ch, start, length);
} else if ("name".equals(nodeName)) {
name.append(ch, start, length);
} else if ("version".equals(nodeName)) {
version.append(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
// 注意这里是和locaName比较,不是和nodeName比较!!!
// 当遇到比如时,localName会自动被置为"app"
if ("app".equals(localName)) {
Log.d("ContentHandler", "id is " + id.toString().trim());
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
// 最后要将StringBuilder清空掉
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
@Override
public void endDocument() throws SAXException {
}
}
// 然后我们在MainActivity里用SAX解析
private void parseXMLWithSAX(String xmlData) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
ContentHandler handler = new ContentHandler();
// 将ContentHandler的实例设置到XMLReader中
xmlReader.setContentHandler(handler);
// 开始执行解析
xmlReader.parse(new InputSource(new StringReader(xmlData)));
} catch ...
}
[{"id":"5","version":"5.5","name":"Angry Birds"},
{"id":"6", "version":"7.0","name":"Clash of Clans"},
{"id":"7", "version":"3.5","name":"Hey Day"}]
// 用JSONObject解析JSON
private void parseJSONWithJSONObject(String jsonData) {
try {
JSONArray jsonArray = new JSONArray(jsonData);
for (int i=0; i"id");
String name = jsonObject.getString("name");
String version = jsonObject.getString("version");
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
} catch ...
}
Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);
如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TyperToken将期望解析成的数据类型传入到fromJson()方法中:
// 其实倒是和泛型容器的初始化很像啊,你看TypeToken的泛型参数就是people容器的类型,还是很美观哒
List people = gson.fromJson(jsonData, new TypeToken>(){}.getType());
// 新增一个App类,并加入id、name和version这三个字段
public class App {
private String id;
private String name;
private String version;
... // 剩下是get和set函数
// 然后就可以解析啦,在MainActivity里
private void parseJSONWithGSON(String jsonData) {
Gson gson = new Gson();
List appList = gson.fromJson(jsonData, new TypeToken>() {}.getType());
for (App app : appList) {
Log.d("MainActivity", "id is " + app.getId());
Log.d("MainActivity", "name is " + app.getName());
Log.d("MainActivity", "version is " + app.getVersion());
}
}
public class HttpUtil {
public static String sendHttpRequest(String address) {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while((line=reader.readLine())!=null) {
response.append(line);
}
return response.toString();
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}
以后每当需要发起一条HTTP请求的时候就可以这样写:
String address = "http://www.baidu.com";
String response = HttpUtil.sendHttpRequest(address);
如果我们在sendHttpRequest()方法中开启了一个线程来发起HTTP请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程中进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的数据了。
那么遇到这种情况应该怎么办呢?其实解决方法并不难,只需要使用Java的回调机制就可以了。
// 第一步,要定义一个接口,比如把它命名成HttpCallbackListener
public interface HttpCallbackListener {
void onFinish(String response);
void onError(Exception e);
}
第二步,修改HttpUtil的静态方法sendHttpRequest(),就是创建子线程来做这些操作,并调用上面接口的方法:
public class HttpUtil {
// 注意子线程是无法通过return语句返回数据的,因而我们将数据传入HttpCallbackListener的onFinish()方法中
public static void sendHttpRequest(final String address, final HttpCallbackListner listener) {
new Thread(new Runnable() {
@Override
public void run() {
... // 做发起HTTP请求的操作,见上面的代码
while ((line = reader.readLine()) != null) {
response.append(line);
}
if (listener != null) {
// 回调onFinish()方法
listener.onFinish(response.toString());
}
} catch (Exception e) {
if (listener != null) {
// 回调onError()方法
listener.onError(e);
}
} finally ...
}).start();
}
}
// 第三步,调用sendHttpRequest()的时候传入一个HttpCallbackListener()的实例,也就是要有onFinish()和onError()的实现
HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
@Override
public void onFinish(String response) {
// 在这里根据返回内容执行具体的逻辑
}
@Override
public void onError(Exception e) {
// 在这里执行对异常的处理
}
});
// onFinish()方法和onError()方最终还是在子线程中执行的,所以还是不能更新UI,要更新的话,在里面构造Message,并调用handler.sendMessage(msg),然后在主线程里通过Handler更新UI
public class MainActivity extends Activity {
private TextView positionTextView;
private LocationManager locationManager;
private String provider;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
positionTextView = (TextView) finViewById(R.id.position_text_view);
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
// 获取所有可用的位置提供器
List providerList = locationManager.getProviders(true);
if (providerList.contains(LocationManager.GPS_PROVIDER))
provider = LocationManager.GPS_PROVIDER;
else if (providerList.contains(LocationManager.NETWORK_PROVIDER))
provider = LocationManager.NETWORK_PROVIDER;
else {
Toast.makeText(this, "No location provider to use", Toast.LENGTH_SHORT).show();
return;
}
// 这个Location对象是动态的哦
Location location = locationManager.getLastKnowLocation(provider);
if (location != null) {
// 显示当前设备的位置信息
showLocation(location);
}
locationManager.requestLocationUpdates(provider, 5000, 1, locationListener);
}
protected void onDestroy() {
super.onDestroy();
if (locationManager != null) {
// 关闭程序时将监听器移除
locationManager.removeUpdates(locationListener);
}
}
LocationListener locationListener = new LocationListener() {
@Override
public void onStatusChanged(String provider, int status, Bundle extras) { }
@Override
public void onProviderEnabled(String provider) { }
@Override
public void onProviderDisabled(String provider) { }
@Override
public void onLocationChanged(Location location) {
// 更新当前设备的位置信息
showLocation(location);
}
};
private void showLocation(Location location) {
String currentPosition = "latitude is " + location.getLatitude() + "\n"
+ "longitude is " + location.getLongitude();
positionTextView.setText(currentPosition);
}
}
获取当前的位置信息也需要声明权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
之后如果你拿着手机随处移动,就可以看到界面上的经纬度信息是会变化的。
http://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&sensor=true_or_false
json表示希望服务器能够返回JSON格式的数据,这里也可以指定成xml。sensor=true_or_false表示这条请求是否来自于某个设备的位置传感器,通常指定成false即可。
这样一条请求给服务器,我们将会得到一段非常长的JSON格式的数据,其中会包括如下部分内容:
"formatted_address" : "277 Bedford Avenue, 布鲁克林纽约州 11211美国"
public class MainActivity extends Activity {
public static final int SHOW_LOCATION = 0;
...
// 永远在子线程里做Http操作
private void showLocation(final Location location) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 组装反向地理编码的接口地址
StringBuilder url = new StringBuilder();
url.append("http://maps.googleapis.com/maps/api/geocode/json?latlng=");
url.append(location.getLatitude()).append(",");
url.append(location.getLongitude());
url.append("&sensor=false");
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url.toString());
// 在请求消息头中指定语言,保证服务器会返回中文数据
httpGet.addHeader("Accept-Language", "zh-CN");
HttpResponse httpResponse = httpClient.execute(httpGet);
if (httpResponse.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity, "utf-8");
JSONObject jsonObject = new JSONObject(response);
// 获取results节点下的位置信息
JSONArray resultArray = jsonObject.getJSONArray("results");
if (resultArray.length() > 0) {
JSONObject subObject = resultArray.getJSONObject(0);
// 取出格式化后的位置信息
String address = subObject.getString("formatted_address");
Message message = new Message();
message.what = SHOW_LOCATION;
message.obj = address;
handler.sendMessage(message);
}
}
} catch ...
}
}).start();
}
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_LOCATION:
String currentPosition = (String) msg.obj;
positionTextView.setText(currentPosition);
break;
default:
break;
}
}
};
}
由于一个经纬度的值有可能包含了好几条街道,因此服务器通常会返回一组位置信息,这些信息都是存放在results节点下的。在得到了这些位置信息后只需要取其中的第一条就可以了,通常这也是最接近我们位置的那一条。
当然,在这个例子中我们只是对服务器返回的JSON数据进行了最简单的解析,位置信息作为整体取出,其实你还可以进行更精确的解析,将国家名、城市名、街道名、甚至邮政编码等作为独立的信息取出。
只不过谷歌地图在2013年3月的时候全面停用了第一版的API Key,而第二版的API Key在中国使用的时候又有诸多限制,因此这里我们就不准备使用谷歌地图了。
<LinearLayout ...>
<com.baidu.mapapi.map.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
LinearLayout>
// 第二步,在MainActivity中初始化地图
public class MainActivity extends Activity {
private MapView mapView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化操作一定要在setContentView()方法前调用
SDKInitializer.initialize(getApplicationContext());
setContentView(R.layout.activity_main);
mapView = (MapView) findViewById(R.id.map_view);
}
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
}
<manifest ...>
...
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<... USE_CREDENTIALS" />
<... MANAGE_ACCOUNTS" />
<... AUTHENTICATE_ACCOUNTS" />
<... ACCESS_NETWORK_STATE" />
<... INTERNET" />
<... CHANGE_WIFI_STATE" />
<... ACCESS_WIFI_STATE" />
<... READ_PHONE_STATE" />
<... WRITE_EXTERNAL_STORAGE" />
<... BROADCAST_STICKY" />
<... WRITE_SETTINGS" />
<... READ_PHONE_STATE" />
<... ACCESS_FINE_LOCATION" />
...
<application ...>
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
androir:value="LTLdkcQP1XMZr3m6bujDB47v" />
...
application>
manifest>
BaiduMap baiduMap = mapView.getMap();
有了BaiduMap后,我们就能对地图进行各种各样的操作了,比如设置地图的缩放级别以及将地图定位到某一个经纬度上。
百度地图将缩放级别的取值范围限定在3到19之间,其中小数点位的值也是可以取得,值越大,地图显示的信息就越精细。比如我们想要将缩放级别设置为12.5:
MapStatusUpdate update = MapStatusUpdateFactory.zoomTo(12.5f);
baiduMap.animateMapStatus(update);
那么怎么才能让地图定位到某一个经纬度上呢?
LatLng ll = new LatLng(39.915, 116.404);
MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
baiduMap.animateMapStatus(update);
上述代码就实现了将地图定位到北纬39.916度、东经116.404度这个位置的功能。
// 第一步,修改11.2.2 确定自己位置的经纬度,把showLocation(location)方法换成navigateTo(location)
public class MainActivity extends Activity {
...
pirvate BaiduMap baiduMap;
...
private boolean isFirstLocate = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
SDKInitializer.initialize(getApplicationContext());
...
baiduMap = mapView.getMap();
...
Location location = locationManager.getLastKnownLocation(provider);
if (location != null) {
navigateTo(location);
}
locationManager.requestLocationUpdates(provider, 5000, 1, locationListener);
}
private void navigateTo(Location location) {
if (isFirstLocate) {
LatLng ll = new LatLng(location.getLatitude(), location.getLongitude());
MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
baiduMap.animateMapStatus(update);
update = MapStatusUpdateFactory.zoomTo(16f);
baiduMap.animateMapStatus(update);
isFirstLocate = false;
}
}
LocationListener locationListener = new LocationListener() {
...
@Override
public void onLocationChanged(Location location) {
// 更新当前设备的位置信息
if (location != null) {
navigateTo(location);
}
}
};
...
}
另外还有一点要注意,上述代码中我们使用了一个isFirstLocate变量,这个变量的作用是为了防止多次调用animateMapStatus()方法,因为将地图移动到我们当前的位置只需要在程序第一次定位的时候调用一次就可以了。
public class MainActivity extends Activity {
...
// 添加让我显示在地图上的功能,修改11.4.3的代码
@Override
protected void onCreate(Bundle savedInstanceState) {
...
baiduMap.setMyLocationEnabled(true);
...
}
private void navigateTo(Location location) {
if (isFirstLocate) {
...
isFirstLocate = false;
}
MyLocationData.Builder locationBuilder = new MyLocationData.Builder();
locationBuilder.latitude(location.getLatitude());
locationBuilder.longitude(location.getLongitude());
MyLocationData locationData = locationBuilder.build();
baiduMap.setMyLocationData(locationData);
}
...
@Override
protected void onDestroy() {
super.onDestroy();
baiduMap.setMyLocationEnabled(false);
mapView.onDestroy();
if (locationManager != null) {
locationManager.removeUpdates(locationListener);
}
}
...
}
注意这段逻辑必须写在isFirstLocate这个if条件语句的外面,因为让地图移动到我们当前的位置只需要在第一次定位的时候执行,但是设备在地图上显示的位置却应该是随着设备的移动而实时改变的。
git branch -a
由于目前BaiduMapTest项目中还没有创建过任何分支,因此只有一个master分支存在,这也就是前面所说的主干线。接下来我们尝试去创建一个分支:
git branch version1.0
这样就创建了一个名为version1.0的分支,我们再次输入git branch -a这个命令来检查一下,可以看到果然有一个叫做version1.0的分支出现了。你会发现,master分支的前面有一个*号,说明目前我们的代码还是在master分支上的,那么怎样才能切换到version1.0这个分支上呢?其实也很简单,只需要使用checkout命令即可:
git checkout version1.0
因此,如果我们在version1.0分支上修改了一个bug,在master分支上这个bug仍然是存在的。这时将修改的代码一行行复制到master分支上显然不是一个聪明的做法,最好的办法就是使用merge命令来完成合并操作:
git checkout master
git merge version1.0
当然,在合并分支的时候还有可能出现代码冲突的情况,这个时候你就需要静下心来慢慢地找出并解决这些冲突,Git在这里就无法帮助你了。
最后,当我们不需要version1.0分支的时候,可以删除这个分支:
git branch -D version1.0
git fetch origin master
执行这个命令后,就会将远程版本库上的代码同步到本地,不过同步下来的代码并不会合并到任何分支上去,而是会存放在一个origin/master分支上,这时我们可以通过diff命令来查看远程版本库上到底修改了哪些东西:
git diff origin/master
之后再调用merge命令将origin/master分支上的修改合并到主分支上即可:
git merge origin/master
而pull命令则相当于将fetch和merge这两个命令放在一起执行了,他可以从远程版本库上获取最新的代码并合并到本地:
git pull origin master
public class MainActivity extends Activity {
private SensorManager sensorManager;
private TextView lightLevel;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (sensorManager != null) {
sensorManager.unregisterListener(listener);
}
}
private SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float value = event.values[0];
lightLevel.setText("Current light level is " + value + " lx");
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { }
};
}
public class MainActivity ... {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
...
}
...
private SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
float xValue = Math.abs(event.values[0]);
float yValue = Math.abs(event.values[1]);
float zValue = Math.abs(event.values[2]);
if (xValue > 15 || yValue > 15 || zValue > 15) {
Toast.makeText(MainActivity.this, "摇一摇", Toast.LENGTH_SHORT).show();
}
}
...
};
}
SensorManager.getRotationMatrix(R, null, accelerometerValues, magneticValues);
得到了R数组之后,接着就可以调用SensorManager的getOrientation()方法来计算手机的旋转数据了。
SensorManager.getOrientation(R, values);
values是一个长度为3的float数组,手机在各个方向上的旋转数据都会被存放到这个数组当中。
// 第一步,它把手机绕Z轴的旋转角度作为指南针的角度显然不对,先这么着吧
public class MainActivity extends Activity {
private SensorManager sensorManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor magneticSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
Sensor accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(listener, magneticSensor, SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(listener, accelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
}
... // onDestroy
private SensorEventListener listener = new SensorEventListener() {
float[] accelerometerValues = new float[3];
float[] magneticValues = new float[3];
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
accelerometerValues = event.values.clone();
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
// 赋值要用clone方法,否则accelerometerValues和magneticValues会指向同一个引用
magneticValues = event.values.clone();
}
float[] R = new float[9];
float[] values = new float[3];
SensorManager.getRotationMatrix(R, null, accelerometerValues, magneticValues);
SensorManager.getOrientation(R, values);
Log.d("MainActivity", "value[0] is " + Math.toDegrees(values[0]));
}
...
};
}
<RelativeLayout ...>
<ImageView
android:id="@+id/compass_img"
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_centerInParent="true"
android:src="@drawable/compass" />
<ImageView
android:id="@+id/arrow_img"
android:layout_width="60dp"
android:layout_height="110dp"
android:layout_centerInParent="true"
android:src="@drawable/arrow" />
RelativeLayout>
// 第三步,修改MainActivity,旋转背景图片咯
private ImageView compassImg;
private SensorEventListener listener = new SensorEventListener() {
...
// 会自动初始化为0.0
private float lastRotateDegree;
@Override
public void onSensorChanged(SensorEvent event) {
...
// 将计算出的旋转角度取反,用于旋转指南针背景图
float rotateDegree = -(float) Math.toDegrees(values[0]);
if (Math.abs(rotateDegree - lastRotateDegree) > 1) {
RotateAnimation animation = new RotateAnimation(lastRotateDegree,
rotateDegree, Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
animation.setFillAfter(true);
compassImg.startAnimation(animation);
lastRotateDegree = rotateDegree;
}
}
...
};
然后在onSensorChanged()方法中使用到了旋转动画技术,我们创建了一个RotateAnimation的实例,并给它的构造方法传入了六个参数,第一个参数表示旋转的起始角度,第二个参数表示旋转的终止角度,后面四个参数用于指定旋转的中心点。这里我们把从传感器中获取到的旋转角度取反,传递给RotateAnimation,并指定旋转的中心点为指南针背景图的中心,然后调用ImageView的startAnimation()方法来执行旋转动画。
// 定制自己的MyApplication,管理全局的状态信息的好方法,比如全局Context
public class MyApplication extends Application {
// 静态context,以使只有一个副本
private static Context context;
@Override
public void onCreate() {
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
我们重写了父类的onCreate()方法,并通过调用getApplicationContext()方法得到了一个应用程序级别的Context,然后又提供了一个静态的getContext()方法,在这里将刚才获取到的Context进行返回。
接下来我们需要告知系统,当程序启动的时候应该初始化MyApplication类,而不是默认的Application类。
<application
android:name="com.example.networktest.MyApplication"
...>
...
application>
这样我们就已经实现了一种全局获取Context的机制,之后不管你想在项目的任何地方使用Context,只需要调用一下MyApplication.getContext()就可以了。
但是不知道你有没有发现,putExtra()方法中所支持的数据类型是有限的,虽然常用的一些数据类型它都会支持,但是当你想去传递一些自定义对象的时候就会发现无从下手。
Person person = (Person) getIntent().getSerializableExtra("person_data");
public class Person implements Parcelable {
private String name;
private int age;
...
// 实现Parcelable接口必须重写describeContents和writeToParcel
@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 CREATOR = new Parcelable.Creator() {
@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];
}
};
}
接下来在FirstActivity中我们仍然可以使用相同的代码来传递Person对象,只不过在SecondActivity中获取对象的时候需要稍加改动,如下:
Person person = (Person) getIntent().getParcelableExtra("person_data");
(这一节我觉得作者的理解是片面的。第一,他说Parcelable方式和Serializable方式不同是前者分解,后者序列化,不知道作者知不知道Serializable接口有writeObject方法和readObject方法,它们的参数分别是ObjectOutputStream和ObjectInputStream,而这两个流写出和读入的方法名正是和Parcelable一模一样,你怎么能说两者不同点就在此呢?你用默认序列化方式,就想当然说Serializable是一团,不是啊,人家也是一个域一个域分开的!)
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.d("TAG", "debug log");
然后我们只需要修改LEVEL常量的值,就可以自由地控制日志的打印行为了。比如让LEVEL等于VERBOSE就可以把所有日志都打印出来,让LEVEL等于NOTHING就可以把所有日志都屏蔽掉。
使用了这种方法之后,刚才所说的那个问题就不复存在了,你只需要在开发阶段将LEVEL指定为VERBOSE,当项目正式上线的时候将LEVEL指定成NOTHING就可以了。
<manifest ...>
<uses-sdk .../>
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.example/broadcastbestpractice" />
<application ...>
<uses-library android:name="android.test.runner" />
application>
manifest>
public class ActivityCollectorTest extends AndroidTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
}
那么该如何编写测试用例呢?其实也很简单,只需要定义一个以test开头的方法,测试框架就会自动调用这个方法了。然后我们在方法中可以通过断言(assert)的形式来期望一个运行结果,再和实际的运行结果进行对比,这样一条测试用例就完成了。
// 在ActivityCollectorTest类里添加一个测试用例,其实就是一个方法
public class ActivityCollectorTest extends AndroidTest {
...
public void testAddActivity() {
assertEquals(0, ActivityCollector.activities.size());
LoginActivity loginActivity = new LoginActivity();
ActivityCollector.addActivity(loginActivity);
assertEquals(1, ActivityCollector.acitivities.size());
}
...
}
现在可以右击测试工程->Run as->Android JUnit Test来运行这个测试用例。
连续添加两次相同活动的实例,这应该算是一种比较特殊的情况了。这是我们觉得ActivityCollector有能力去过滤掉重复的数据,因此在断言的时候认为目前ActivityCollector中的活动个数仍然是1。重新运行一遍测试用例,没跑通。从这个测试用例中我们发现,addActivity()方法中的代码原来是不够健壮的,这个时候就应该对代码进行优化了。(加上一个contains的判断)