Open rainit2006 opened 6 years ago
注意事项:
由于JSON中的一些字段可能不太适合直接作为Java字段来命名,所以使用@SerializedName注解的方式来让JSON字段和Java字段之间建立映射关系。 比如对于json格式,
"basic":{
"city":"苏州",
"id":"CN101190401",
"update":{
"loc":"2016-08-08 21:58"
}
}
创建对应的类为:
public class Basic {
@SerializedName("city")
public String cityName;
@SerializedName("id")
public String weatherId;
public Update update;
public class Update {
@SerializedName("loc")
public String updateTime;
}
}
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/title" />
<include layout="@layout/now" />
<include layout="@layout/forecast" />
<include layout="@layout/aqi" />
<include layout="@layout/suggestion" />
</LinearLayout>
■Log Android中的日志工具类是Log(android.util.Log),这个类中提供了如下5个方法来供我们打印日志。
Log.v():用于打印那些最为琐碎的、意义最小的日志信息。对应级别verbose,是Android日志里面级别最低的一种。 Log.d():用于打印一些调试信息,这些信息对你的调试和分析问题应该是有帮助的,对应级别debug,比verbose高一级。 Log.i():用于打印一些比较重要的数据,这些数据应该是你非常想看到的、可以帮助你分析用户行为数据。对应级别info,比debug高一级。 Log.w():用于打印一些警告信息,提示程序在这个地方可能会有潜在的风险,最好去修复一下这些出现警告的地方。对应级别warn,比info高一级。 Log.e():用于打印程序中的错误信息,比如程序进入到了catch语句当中。当有错误信息打印出来的时候,一般都代表你的程序出现严重的问题了,必须尽快修复,对应级别error,比warn高一级。
Log.d()方法中传入了两个参数:第一个参数是tag,一般传入当前的类名就好,主要作用用于对打印信息进行过滤;第二个参数是msg,即想要打印的具体的内容。
Note:不要使用system.out.printIn来打印日志!
■Toast
Toast toast = Toast.makeText(this(一般是xxxActivitiy. this), message, Toast.LENGTH_LONG);
// 位置調整
toast.setGravity(Gravity.CENTER, x, y);
toast.show();
■Intent インテントには「意図」「目的」という意味があります。主にアクティビティを起動する際のパラメータに使われます。
显式Intent:即直接指定需要打开的activity对应的类。
隐式Intent:隐式不明确指定启动哪个Activity,而是设置Action、Data、Category,让系统来筛选出合适的Activity。筛选是根据所有的
一般的なインテント:アラーム、カレンダ、カメラ、Webブラウザ。。。 https://developer.android.com/guide/components/intents-common?hl=ja
数据传递: 渡すときはputExtra()のメソッドのみだが取り出す際は int型ならgetIntExtra(""); String型ならgetStringExtra(""); のように型に対応したメソッドを呼び出す。
startActiivityForResult(requestcode,intnet): 当子窗体关闭时,父窗体会执行onActivityResult()方法,并可以获取子窗体的返回值.
public void onClick(View v) {
//得到新打开Activity关闭后返回的数据
//第二个参数为请求码,可以根据业务需求自己编号
startActivityForResult(new Intent(MainActivity.this, OtherActivity.class), 1);
}
});
public void onClick(View v) {
//数据是使用Intent返回
Intent intent = new Intent();
//把返回数据存入Intent
intent.putExtra("result", "My name is luis");
//设置返回数据
OtherActivity.this.setResult(RESULT_OK, intent);//RESULT_OK为自定义常量
//关闭Activity
OtherActivity.this.finish();
}
});
String result = data.getExtras().getString("result");//得到新Activity 关闭后返回的数据
请求码的作用 : 使用startActivityForResult(Intent intent, int requestCode)方法打开新的Activity,我们需要为startActivityForResult()方法传入一个请求码(第二个参数)。请求码的值是根据业务需要由自已设定,用于标识请求来源。例如:一个Activity有两个按钮,点击这两个按钮都会打开同一个Activity,不管是那个按钮打开新Activity,当这个新Activity关闭后,系统都会调用前面Activity的onActivityResult(int requestCode, int resultCode, Intent data)方法。在onActivityResult()方法如果需要知道新Activity是由那个按钮打开的,并且要做出相应的业务处理,这时可以这样做:
@Override public void onCreate(Bundle savedInstanceState) {
....
button1.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
startActivityForResult (new Intent(MainActivity.this, NewActivity.class), 1);
}
});
button2.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
startActivityForResult (new Intent(MainActivity.this, NewActivity.class), 2);
}
});
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode){
case 1:
//来自按钮1的请求,作相应业务处理
case 2:
//来自按钮2的请求,作相应业务处理
}
}
}
作者:梦沉薇露 链接:https://www.jianshu.com/p/75eccd29c229
■ 生命周期 onSaveInstanceState()方法会携带一个 Bundle 类型的参数。 活动被回收后,再次进入会再次进入oncreate()方法。
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}
数据是已经保存下来了,那么我们应该在哪里进行恢复呢?其实我们一直使用的 onCreate()方法其实也有一个 Bundle 类型的参数。这个参数在一般情况下都是null,但是当活动被系统回收之前有通过 onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG, tempData);
}
……
}
取出值之后再做相应的恢复操作就可以了。
■活动启动模式: 活动activity中配置android:launchMode="xxxx"来指点启动模式。 standard, 每次启动都创建该活动的一个新的实例放入栈顶。 singleTop : 启动时,若发现栈顶已经是该活动,则直接使用不再创建。若该活动不在栈顶,则仍需新建。 singleTask:启动时,检查栈中是否有该活动,有则直接使用(onRestart()),并将其上的所有活动出栈(onDestroy())。 即其他活动onDestory(),该活动onRestart(); singleInstance :指定为 singleInstance 模式的活动会启用一个新的返回栈来管理这个活动。 场景:程序中的一个活动允许其他活动调用,其他程序和我们的程序共享该活动的实例。 每个应用程序都有自己的返回栈,同一活动在不同的返回栈中入栈必然会创建新的实例,该模式可以解决这个问题。该模式下会有一个单独的返回栈来管理这个活动,不论哪个程序来访问这个活动,都公用同一个返回栈。
■知晓当前是哪个活动
public class BaseActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
//在BaseActivity的onCreate()方法中添加:
Log.d("BaseActivity",getClass().getSimpleName());
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
然后让 BaseActivity 成为项目中所有活动的父类。这样子类活动时,log里就可以打印出相应的类名。
■随时随地退出程序: 自定义一个活动管理器ActivityCollector
public class ActivityCollector {
private static List<Activity> ls = new ArrayList<Activity>();
public static void addActivity(Activity activity){
ls.add(activity);
}
public static void removeActivity(Activity activity){
ls.remove(activity);
}
public static void finishAll(){
for(Activity activity : ls){
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);
}
}
以后在需要的时候,只调用ActivityController.finishAll() 就可以。
■启动活动的最佳写法
public class SecondActivity extends BaseActivity {
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}
...
}
SecondActivity所需要的数据在方法参数中全部体现出来了,这样即使不用阅读SecondActivity中的代码,不去询问负责编写SecondActivity的同事,你也可以非常清晰地知道启动SecondActivity需要传递哪些数据。另外,这样写还简化了启动活动的代码,现在只需要一行代码就可以启动SecondActivity,如下所示:
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SecondActivity.actionStart(FirstActivity.this, "data1", "data2");
}
});
■引入布局 新建一个布局title.xml。在activity_main.xml中的代码里使用。
<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 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();
}
});
}
}
改activity_main.xml中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
这样每当我们在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。
■ ListView的使用
基本使用方法 数组中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。 注意,我们使用了android.R.layout.simple_list_item_1 作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。
public class MainActivity extends AppCompatActivity {
private String[] data = { "Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"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<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, data);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
- 定制ListView的界面
定义一个实体类,作为ListView适配器的适配类型
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
Fruit 类中只有两个字段,name 表示水果的名字,imageId 表示水果对应图片的资源id。
为ListView的子项指定一个我们自定义的布局,在layout目录下新建fruit_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content">
<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_vertical"
android:layout_marginLeft="10dp" />
接下来需要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit 类。新建类FruitAdapter ,代码如下所示:
public class FruitAdapter extends ArrayAdapter
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId,
List
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前项的Fruit实例
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent,
false);
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;
}
}
下面修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
private List<Fruit> 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() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit("Banana", R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit("Orange", R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
fruitList.add(mango);
}
}
}
这里添加了一个initFruits() 方法,用于初始化所有的水果数据。
- 提升ListView的运行效率
之所以说ListView这个控件很难用,就是因为它有很多细节可以优化,其中运行效率就是很重要的一点。目前我们ListView的运行效率是很低的,因为在FruitAdapter 的getView() 方法中,每次都将布局重新加载了一遍,当ListView快速滚动的时候,这就会成为性能的瓶颈。
仔细观察会发现,getView() 方法中还有一个convertView 参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用。修改FruitAdapter 中的代码,如下所示:
public class FruitAdapter extends ArrayAdapter
...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent,
false);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}
不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是每次在getView() 方法中还是会调用View 的findViewById() 方法来获取一次控件的实例。我们可以借助一个ViewHolder 来对这部分性能进行优化,修改FruitAdapter 中的代码,如下所示:
public class FruitAdapter extends ArrayAdapter
...
@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, parent,
false);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById
(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById (R.id.fruit_name);
view.setTag(viewHolder); // 将ViewHolder存储在View中
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag(); // 重新获取ViewHolder
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder {
ImageView fruitImage;
TextView fruitName;
}
}
我们新增了一个内部类ViewHolder ,用于对控件的实例进行缓存。当convertView 为null 的时候,创建一个ViewHolder 对象,并将控件的实例都存放在ViewHolder 里,然后调用View 的setTag() 方法,将ViewHolder 对象存储在View 中。当convertView 不为null 的时候,则调用View 的getTag() 方法,把ViewHolder 重新取出。这样所有控件的实例都缓存在了ViewHolder 里,就没有必要每次都通过findViewById() 方法来获取控件实例了。
通过这两步优化之后,我们ListView的运行效率就已经非常不错了。
- ListView的点击事件
MainActivity中的onCreate代码里:
追加 listView.setOnItemClickListener(new AdapterView.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(); } }); }
■ RecyclerView的使用 Android提供了一个更强大的滚动控件——RecyclerView。它可以说是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。目前Android官方更加推荐使用RecyclerView。
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:27.1.1'
compile 'com.android.support:recyclerview-v7:27.1.1'
testCompile 'junit:junit:4.12'
}
注意: 如何确认依赖包的版本号? https://blog.csdn.net/ss1168805219/article/details/72621854
添加完之后记得要点击一下Sync Now来进行同步。然后修改activity_main.xml中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
接下来需要为RecyclerView准备一个适配器,新建FruitAdapter 类,让这个适配器继承自RecyclerView.Adapter ,并将泛型指定为FruitAdapter.ViewHolder 。其中,ViewHolder 是我们在FruitAdapter 中定义的一个内部类,代码如下所示:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
mFruitList = fruitList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fruit_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
由于FruitAdapter 是继承自RecyclerView.Adapter 的,那么就必须重写onCreateViewHolder() 、onBindViewHolder() 和getItemCount() 这3个方法。onCreateViewHolder() 方法是用于创建ViewHolder 实例的,我们在这个方法中将fruit_item 布局加载进来,然后创建一个ViewHolder 实例,并把加载出来的布局传入到构造函数当中,最后将ViewHolder 的实例返回。onBindViewHolder() 方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position 参数得到当前项的Fruit 实例,然后再将数据设置到ViewHolder 的ImageView和TextView当中即可。getItemCount() 方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits(); // 初始化水果数据
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit("Banana", R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit("Orange", R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
fruitList.add(mango);
}
}
}
实现横向滚动和瀑布流布局 ListView的扩展性并不好,它只能实现纵向滚动的效果,如果想进行横向滚动的话,ListView就做不到了。那么RecyclerView就能做得到吗?当然可以,不仅能做得到,还非常简单,那么接下来我们就尝试实现一下横向滚动的效果。 首先要对fruit_item 布局进行修改,因为目前这个布局里面的元素是水平排列的,适用于纵向滚动的场景,而如果我们要实现横向滚动的话,应该把fruit_item 里的元素改成垂直排列才比较合理。修改fruit_item.xml中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="100dp"
android:layout_height="wrap_content" >
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />
</LinearLayout>
接下来修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
...
}
MainActivity中只加入了一行代码,调用LinearLayoutManager的setOrientation() 方法来设置布局的排列方向,默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL 表示让布局横行排列,这样RecyclerView就可以横向滚动了。
为什么ListView很难或者根本无法实现的效果在RecyclerView上这么轻松就能实现了呢?这主要得益于RecyclerView出色的设计。ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager,LayoutManager中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。
除了LinearLayoutManager之外,RecyclerView还给我们提供了GridLayoutManager和StaggeredGridLayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。
修改FruitAdapter 中的代码,如下所示:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
View fruitView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitView = view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
mFruitList = fruitList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.
fruit_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked view " + fruit.getName(),
Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked image " + fruit.getName(),
Toast.LENGTH_SHORT).show();
}
});
return holder;
}
...
}
这里分别为最外层布局和ImageView都注册了点击事件,RecyclerView的强大之处也在这里,它可以轻松实现子项中任意控件或布局的点击事件。
■使用draw9patch编辑message_left图片 在Android sdk目录下有一个tools文件夹,在这个文件夹中找到draw9patch.bat文件.
我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容会被放置的区域。
然后把该图片作为view item的背景图
效果: 这样当图片需要拉伸的时候,就可以只拉伸指定的区域,程序在外观上也有了很大的改进。
■Fragment 有两个不同包下的Fragment供你选择,一个是系统内置的android.app.Fragment,一个是support-v4库中的android.support.v4.app.Fragment。这里我强烈建议你使用support-v4库中的Fragment,因为它可以让碎片在所有Android系统版本中保持功能一致性。比如说在Fragment中嵌套使用Fragment,这个功能是在Android 4.2系统中才开始支持的,如果你使用的是系统内置的Fragment,那么很遗憾,4.2系统之前的设备运行你的程序就会崩溃。而使用support-v4库中的Fragment就不会出现这个问题,只要你保证使用的是最新的support-v4库就可以了。另外,我们并不需要在build.gradle文件中添加support-v4库的依赖,因为build.gradle文件中已经添加了appcompat-v7库的依赖,而这个库会将support-v4库也一起引入进来。
基本用法: 1, 新建一个左侧碎片布局left_fragment.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Button"
/>
</LinearLayout>
2, 然后新建右侧碎片布局right_fragment.xml。代码略。 3, 新建一个LeftFragment 类,并让它继承自Fragment。
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);
//通过LayoutInflater的inflate() 方法将刚才定义的left_fragment布局动态加载进来.
return view;
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
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" />
使用了<fragment> 标签在布局中添加碎片,通过android:name 属性来显式指明要添加的碎片类名,注意一定要将类的包名也加上。
完成,效果图:
动态添加碎片
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
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 //注意这里是FrameLayout,是一个layout。 android:id="@+id/right_layout" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" >
3,修改MainActivity中的代码
public class MainActivity extends AppCompatActivity implements View.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); replaceFragment(new RightFragment()); }
@Override public void onClick(View v) { switch (v.getId()) { case R.id.button: replaceFragment(new AnotherRightFragment()); break; default: break; } } private void replaceFragment(Fragment fragment) { FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.replace(R.id.right_layout, fragment); transaction.commit(); } }
结合replaceFragment() 方法中的代码可以看出,动态添加碎片主要分为5步。
(1) 创建待添加的碎片实例。
(2) 获取FragmentManager,在Activity中可以直接通过调用getSupportFragmentManager() 方法得到。
(3) 开启一个事务,通过调用beginTransaction() 方法开启。
(4) 向容器内添加或替换碎片,一般使用replace() 方法实现,需要传入容器的id和待添加的碎片实例。
(5) 提交事务,调用commit() 方法来完成。
在碎片中模拟返回栈 通过点击按钮添加了一个碎片之后,这时按下Back键程序就会直接退出。如果这里我们想模仿类似于返回栈的效果,按下Back键可以回到上一个碎片,该如何实现呢? 其实很简单,FragmentTransaction中提供了一个addToBackStack() 方法,可以用于将一个事务添加到返回栈中,修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private void replaceFragment(Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
这里我们在事务提交之前调用了FragmentTransaction的addToBackStack() 方法,它可以接收一个名字用于描述返回栈的状态,一般传入null 即可。 现在重新运行程序,并点击按钮将AnotherRightFragment添加到活动中,然后按下Back键,你会发现程序并没有退出,而是回到了RightFragment界面,继续按下Back键,RightFragment界面也会消失,再次按下Back键,程序才会退出。
碎片和活动之间进行通信 为了方便碎片和活动之间进行通信,FragmentManager提供了一个类似于findViewById() 的方法,专门用于从布局文件中获取碎片的实例,代码如下所示:
RightFragment rightFragment = (RightFragment) getSupportFragmentManager()
.findFragmentById(R.id.right_fragment);
同理,在每个碎片中都可以通过调用getActivity() 方法来得到和当前碎片相关联的活动实例
MainActivity activity = (MainActivity) getActivity();
另外当碎片中需要使用Context 对象时,也可以使用getActivity() 方法,因为获取到的活动本身就是一个Context 对象。
当RightFragment第一次被加载到屏幕上时,会依次执行onAttach() 、onCreate() 、onCreateView() 、onActivityCreated() 、onStart() 和onResume() 方法 然后点击LeftFragment中的按钮,由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause() 、onStop() 和onDestroyView() 方法会得到执行。当然如果在替换的时候没有调用addToBackStack() 方法,此时的RightFragment就会进入销毁状态,onDestroy() 和onDetach() 方法就会得到执行。 接着按下Back键,RightFragment会重新回到屏幕,会执行onCreateView() 、onActivityCreated() 、onStart() 和onResume() 方法。注意此时onCreate() 方法并不会执行,因为我们借助了addToBackStack() 方法使得RightFragment并没有被销毁。 现在再次按下Back键,依次会执行onPause() 、onStop() 、onDestroyView() 、onDestroy() 和onDetach() 方法,最终将碎片销毁掉。
使用限定符 怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定符(Qualifiers)来实现了。 1, 修改FragmentTest项目中的activity_main.xml文件,只留下一个左侧碎片,并让它充满整个父布局。 2,接着在res目录下新建layout-large文件夹,在这个文件夹下新建一个布局,也叫作activity_main.xml,这里包含了两个碎片,即双页模式。 3, 然后将MainActivity中replaceFragment() 方法里的代码注释掉。 其中large 就是一个限定符,那些屏幕被认为是large 的设备就会自动加载layout-large文件夹下的布局,而小屏幕的设备则还是会加载layout文件夹下的布局。
使用最小宽度限定符 又有一个新的问题出现了,large 到底是指多大呢?有的时候我们希望可以更加灵活地为不同设备加载布局,不管它们是不是被系统认定为large ,这时就可以使用最小宽度限定符(Smallest-width Qualifier)了。 最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
例:在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity_main.xml布局。 这就意味着,当程序运行在屏幕宽度大于600dp的设备上时,会加载layout-sw600dp/activity_main布局,当程序运行在屏幕宽度小于600dp的设备上时,则仍然加载默认的layout/activity_main布局。
广播机制 ■ Android中的广播主要可以分为两种类型:标准广播和有序广播。 标准广播 (Normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。 有序广播 (Ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。
Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,时间或时区发生改变也会发出一条广播,等等。如果想要接收到这些广播,就需要使用广播接收器
■ 接收系统广播
那么该如何创建一个广播接收器呢?其实只需要新建一个类,让它继承自BroadcastReceiver ,并重写父类的onReceive() 方法就行了。这样当有广播到来时,onReceive() 方法就会得到执行,具体的逻辑就可以在这个方法中处理。
动态注册 例:新建一个BroadcastTest项目,然后修改MainActivity中的代码。
public class MainActivity extends AppCompatActivity {
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);
}
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();
}
}
}
}
首先我们创建了一个IntentFilter 的实例,并给它添加了一个值android.net.conn.CONNECTIVITY_CHANGE 的action,为什么要添加这个值呢?因为当网络状态发生变化时,系统发出的正是一条值为android.net.conn.CONNECTIVITY_CHANGE 的广播, 也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的action。 动态注册的广播接收器一定都要取消注册才行,这里我们是在onDestroy() 方法中通过调用unregisterReceiver() 方法来实现的。
在onReceive() 方法中,首先通过getSystemService() 方法得到了ConnectivityManager 的实例,这是一个系统服务类,专门用于管理网络连接的。然后调用它的getActiveNetworkInfo() 方法可以得到NetworkInfo 的实例,接着调用NetworkInfo 的isAvailable() 方法,就可以判断出当前是否有网络了,最后我们还是通过Toast的方式对用户进行提示。
还要注意,需要在AndroidManifest.xml文件里面加入如下权限以访问系统网络状态
可以使用Android Studio提供的快捷方式来创建一个广播接收器,右击com.example.broadcasttest包→New→Other→Broadcast Receiver。 静态的广播接收器一定要在AndroidManifest.xml文件中注册才可以使用,不过由于我们是使用Android Studio的快捷方式创建的广播接收器,因此注册这一步已经被自动完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
//为了接收到boot消息
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
可以看到,
需要注意的是,不要在onReceive() 方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive() 方法运行了较长时间而没有结束时,程序就会报错。因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动一个服务等。
■ 发送自定义广播
发送标准广播
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override public void onReceive(Context context, Intent intent) { Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_ SHORT).show(); } }
2, 在AndroidManifest.xml中对这个广播接收器进行修改,让MyBroadcastReceiver接收一条值为com.example.broadcasttest.MY_BROADCAST 的广播
3,接下来修改activity_main.xml中的代码,在布局文件中定义了一个按钮,用于作为发送广播的触发点。
4, 然后修改MainActivity中的代码
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST"); sendBroadcast(intent); } }); ...
可以看到,我们在按钮的点击事件里面加入了发送自定义广播的逻辑。首先构建出了一个Intent 对象,并把要发送的广播的值传入,然后调用了Context的sendBroadcast() 方法将广播发送出去,这样所有监听com.example.broadcasttest.MY_BROADCAST 这条广播的广播接收器就会收到消息。此时发出去的广播就是一条标准广播。
发送有序广播
■使用本地广播 为了能够简单地解决广播的安全性问题,Android引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播,这样所有的安全性问题就都不存在了。
本地广播的用法并不复杂,主要就是使用了一个LocalBroadcastManager来对广播进行管理,并提供了发送广播和注册广播接收器的方法。
修改MainActivity中的代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this); // 获取实例
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.LOCAL_
BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 发送本地广播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注
册本地广播监听器
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).
show();
}
另外还有一点需要说明,本地广播是无法通过静态注册的方式来接收的。其实这也完全可以理解,因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,而发送本地广播时,我们的程序肯定是已经启动了,因此也完全不需要使用静态注册的功能。
数据持久化 Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储、SharedPreferences存储以及数据库存储。当然,除了这3种方式之外,你还可以将数据保存在手机的SD卡中. ■文件存储
核心技术就是Context 类中提供的openFileInput() 和openFileOutput() 方法,之后就是利用Java的各种流来进行读写操作。
Context 类中提供了一个openFileOutput() 方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/
注意: 其实文件的操作模式本来还有另外两种:MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞,已在Android 4.2版本中被废弃。
openFileOutput () 方法返回的是一个FileOutputStream 对象,得到了这个对象之后就可以使用Java流的方式将数据写入到文件中了。
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();
}
}
}
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();
}
处理思路非常简单,在onCreate() 方法中调用load() 方法来读取文件中存储的文本内容,如果读到的内容不为null ,就调用EditText的setText() 方法将内容填充到EditText里,并调用setSelection() 方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出一句还原成功的提示。
■ SharedPreferences存储
不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。
- 将数据存储到SharedPreferences中
要想使用SharedPreferences来存储数据,首先需要获取到SharedPreferences 对象。Android中主要提供了3种方法用于得到SharedPreferences 对象。
01. Context 类中的getSharedPreferences() 方法
此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下的。第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
其他几种操作模式均已被废弃,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种模式是在Android 4.2版本中被废弃的,MODE_MULTI_PROCESS模式是在Android 6.0版本中被废弃的。
02. Activity 类中的getPreferences() 方法
03. PreferenceManager 类中的getDefaultSharedPreferences() 方法
例子:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button saveData = (Button) findViewById(R.id.save_data); saveData.setOnClickListener(new View.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.apply(); } }); }
利用File Explorer来 /data/data/com.example.sharedpreferencestest/shared_prefs/目录下,可以看到生成了一个data.xml文件。
- 从SharedPreferences中读取数据
... Button restoreData = (Button) findViewById(R.id.restore_data); restoreData.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE); String name = pref.getString("name", ""); int age = pref.getInt("age", 0); boolean married = pref.getBoolean("married", false); Log.d("MainActivity", "name is " + name); Log.d("MainActivity", "age is " + age); Log.d("MainActivity", "married is " + married); } });
■ SQLite数据库存储
- SQLiteOpenHelper
Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类就可以非常简单地对数据库进行创建和升级。
SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreate() 和onUpgrade() ,我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase() 和getWritableDatabase() 。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。
- 创建数据库
1. 创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表,表中有id(主键)、作者、价格、页数和书名等列。
create table Book ( id integer primary key autoincrement, author text, price real, pages integer, name text)
2. 新建MyDatabaseHelper 类继承自SQLiteOpenHelper
public class MyDatabaseHelper extends SQLiteOpenHelper { public static final String CREATE_BOOK = "create table Book ("
"name text)"; private Context mContext;
public MyDatabaseHelper(Context context, String name, SQLiteDatabase.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) { } }
2. 修改activity_main.xml中的代码,追加一个按钮(Create database按钮)
3. 修改MainActivity中的代码
@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 View.OnClickListener() { @Override public void onClick(View v) { dbHelper.getWritableDatabase(); } }); }
这里我们在onCreate() 方法中构建了一个MyDatabaseHelper 对象,并且通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后在Create database按钮的点击事件里调用了getWritableDatabase() 方法。这样当第一次点击Create database按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate() 方法,这样Book表也就得到了创建,然后会弹出一个Toast提示创建成功。再次点击Create database按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。
- 升级数据库
在MyDatabaseHelper类修改onUpgrade函数:
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); } }
并修改MainActivity中的代dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dbHelper.getWritableDatabase();
}
还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade() 方法得到执行了。
这里将数据库版本号指定为2,表示我们对数据库进行升级了。
- 添加数据
调用SQLiteOpenHelper的getReadableDatabase() 或getWritableDatabase() 方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个SQLiteDatabase 对象,借助这个对象就可以对数据进行CRUD操作了。使用ContentValues 来对要添加的数据进行组装。
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
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);
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() 方法,用于对数据进行更新。
Button updateData = (Button) findViewById(R.id.update_data); updateData.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("price", 10.99); db.update("Book", values, "name = ?", new String[] { "The Da Vinci Code" }); } });
上述代码想表达的意图是将名字是The Da Vinci Code的这本书的价格改成10.99。
- 删除数据
SQLiteDatabase 中提供了一个delete() 方法,专门用于删除数据,这个方法接收3个参数,第一个参数仍然是表名,这个已经没什么好说的了,第二、第三个参数又是用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。
Button deleteButton = (Button) findViewById(R.id.delete_data); deleteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.delete("Book", "pages > ?", new String[] { "500" }); } });
指明去删除Book表中的数据,并且通过第二、第三个参数来指定仅删除那些页数超过500页的书。
- 查询数据
SQLiteDatabase中还提供了一个query() 方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。
![image](https://user-images.githubusercontent.com/12871721/45200016-ccccef80-b2a9-11e8-912f-c5dec9f5d63c.png)
Button queryButton = (Button) findViewById(R.id.query_data); queryButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SQLiteDatabase db = dbHelper.getWritableDatabase(); // 查询Book表中所有的数据 Cursor cursor = db.query("Book", null, null, null, null, null, null); if (cursor.moveToFirst()) { do { // 遍历Cursor对象,取出数据并打印 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 ("price")); Log.d("MainActivity", "book name is " + name); Log.d("MainActivity", "book author is " + author); Log.d("MainActivity", "book pages is " + pages); Log.d("MainActivity", "book price is " + price); } while (cursor.moveToNext()); } cursor.close(); } }); }
- 使用SQL操作数据库
Android充分考虑到了你们的编程习惯,同样提供了一系列的方法,使得可以直接通过SQL来操作数据库。
添加数据的方法如下:
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
删除数据的方法如下:
db.execSQL("delete from Book where pages > ?", new String[] { "500" });
查询数据的方法如下:
db.rawQuery("select * from Book", null);
可以看到,除了查询数据的时候调用的是SQLiteDatabase的rawQuery() 方法,其他的操作都是调用的execSQL() 方法。
■使用LitePal操作数据库 LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表和増删改查的操作。LitePal的项目主页上也有详细的使用文档,地址是:https://github.com/LitePalFramework/LitePal 。
■ 配置LitePal 大多数的开源项目都会将版本提交到jcenter上,我们只需要在app/build.gradle文件中声明该开源库的引用就可以了。 编辑app/build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.2.0'
testCompile 'junit:junit:4.12'
compile 'org.litepal.android:core:1.4.1'
}
接下来需要配置litepal.xml文件。右击app/src/main目录→New→Directory,创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件,接着编辑litepal.xml文件中的内容,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" ></dbname>
<version value="1" ></version>
<list>
</list>
</litepal>
其中, 标签用于指定所有的映射模型,我们稍后就会用到。
最后还需要再配置一下LitePalApplication,修改AndroidManifest.xml中的代码,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.litepaltest">
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>
这里我们将项目的application 配置为org.litepal.LitePalApplication ,这样才能让LitePal的所有功能都可以正常工作。
创建和升级数据库 LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。 在前面的例子里,为了创建一张Book表,需要先分析表中应该包含哪些列,然后再编写出一条建表语句,最后在自定义的SQLiteOpenHelper中去执行这条建表语句。但是使用LitePal,你就可以用面向对象的思维来实现同样的功能了。 1,定义一个Book类 (是一个典型的Java bean) 在Book 类中我们定义了id 、author 、price 、pages 、name 这几个字段,并生成了相应的getter 和setter 方法。 生成getter 和setter 方法的快捷方式是,先将类中的字段定义好,然后按下Alt + Insert键(Mac系统是command + N),在弹出菜单中选择Getter and Setter,接着使用Shift键将所有字段都选中,最后点击OK。 2,需要将Book 类添加到映射模型列表当中,修改litepal.xml中的代码
<litepal>
<dbname value="BookStore" ></dbname>
<version value="1" ></version>
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
</list>
</litepal>
这里使用 标签下即可。
3, 现在只要进行任意一次数据库的操作,BookStore.db数据库应该就会自动创建出来。那么我们修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button createDatabase = (Button) findViewById(R.id.create_database);
createDatabase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LitePal.getDatabase();
}
});
}
}
调用LitePal.getDatabase() 方法就是一次最简单的数据库操作,只要点击一下按钮,数据库就会自动创建完成了。
LitePal来升级数据库非常非常简单,你完全不用思考任何的逻辑,只需要改你想改的任何内容,然后将版本号加1就行了。 比如我们想要向Book表中添加一个press(出版社)列,直接修改Book 类中的代码,添加一个press 字段即可,如下所示:
public class Book {
...
private String press;
...
public String getPress() {
return press;
}
public void setPress(String press) {
this.press = press;
}
}
与此同时,我们还想再添加一张Category表,那么只需要新建一个Category 类就可以了,代码略。 然后修改litepal.xml中的代码,把Category追加到list里。
<litepal>
<dbname value="BookStore" ></dbname>
<version value="2" ></version> //版本号提升了!
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
<mapping class="com.example.litepaltest.Category"></mapping>
现在重新运行一下程序,然后点击Create database按钮。可以看到,book表中新增了一个press列,category表也创建成功了,当然LitePal还自动帮我们做了一项非常重要的工作,就是保留之前表中的所有数据,这样就再也不用担心数据丢失的问题了。
使用LitePal添加数据 首先回顾一下之前添加数据的方法,我们需要创建出一个ContentValues 对象,然后将所有要添加的数据put到这个ContentValues 对象当中,最后再调用SQLiteDatabase的insert() 方法将数据添加到数据库表当中。 而使用LitePal来添加数据,这些操作可以简单到让你惊叹!我们只需要创建出模型类的实例,再将所有要存储的数据设置好,最后调用一下save() 方法就可以了。 1, 首先修改模型类,让其继承自DataSupport 类。 LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了,必须要继承自DataSupport 类才行,因此这里我们需要先把继承结构给加上。修改Book 类中的代码,如下所示:
public class Book extends DataSupport {
...
}
2, 接着我们开始向Book表中添加数据,修改MainActivity中的代码
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Book book = new Book();
book.setName("The Da Vinci Code");
book.setAuthor("Dan Brown");
book.setPages(454);
book.setPrice(16.96);
book.setPress("Unknow");
book.save();
}
});
}
调用book.save() 方法就能完成数据添加操作了。那么这个save() 方法是从哪儿来的呢?当然是从DataSupport 类中继承而来的了。除了save() 方法之外,DataSupport 类还给我们提供了丰富的CRUD方法,这些我们在后面都会学到。
使用LitePal更新数据 最简单的一种更新方式就是对已存储的对象重新设值,然后重新调用save() 方法即可。
什么是已存储的对象? 对于LitePal来说,对象是否已存储就是根据调用model.isSaved() 方法的结果来判断的,返回true 就表示已存储,返回false 就表示未存储。 实际上只有在两种情况下model.isSaved() 方法才会返回true ,一种情况是已经调用过model.save() 方法去添加数据了,此时model 会被认为是已存储的对象。另一种情况是model 对象是通过LitePal提供的查询API查出来的,由于是从数据库中查到的对象,因此也会被认为是已存储的对象。
另外一种更加灵巧的更新方式。修改MainActivity中的代码
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Book book = new Book();
book.setPrice(14.95);
book.setPress("Anchor");
book.updateAll("name = ? and author = ?", "The Lost Symbol", "Dan
Brown");
}
});
注意updateAll() 方法中可以指定一个条件约束,和SQLiteDatabase中update() 方法的where 参数部分有点类似,但更加简洁,如果不指定条件语句的话,就表示更新所有数据。这里我们指定将所有书名是The Lost Symbol并且作者是Dan Brown的书价格更新为14.95,出版社更新为Anchor。
在使用updateAll() 方法时,还有一个非常重要的知识点是你需要知晓的,就是当你想把一个字段的值更新成默认值时,是不可以使用上面的方式来set 数据的。我们都知道,在Java中任何一种数据类型的字段都会有默认值,例如int 类型的默认值是0,boolean 类型的默认值是false ,String 类型的默认值是null 。那么当new出一个Book 对象时,其实所有字段都已经被初识化成默认值了,比如说pages 字段的值就是0。因此,如果我们想把数据库表中的pages 列更新成0,直接调用book.setPages(0) 是不可以的,因为即使不调用这行代码,pages 字段本身也是0,LitePal此时是不会对这个列进行更新的。对于所有想要将为数据更新成默认值的操作,LitePal统一提供了一个setToDefault() 方法,然后传入相应的列名就可以实现了。比如我们可以这样写:
Book book = new Book();
book.setToDefault("pages");
book.updateAll();
这段代码的意思是,将所有书的页数都更新为0,因为updateAll() 方法中没有指定约束条件,因此更新操作对所有数据都生效了。
使用LitePal删除数据
public void onClick(View v) {
DataSupport.deleteAll(Book.class, "price < ?", "15");
}
另外,deleteAll() 方法如果不指定约束条件,就意味着你要删除表中的所有数据,这一点和updateAll() 方法是比较相似的。
使用LitePal查询数据
LitePal在查询API方面做了非常多的优化,基本上可以满足绝大多数场景的查询需求,并且代码十分整洁。
查询整个Book表
List<Book> books = DataSupport.findAll(Book.class);
想要查询Book表中的第一条数据就可以这样写:
Book firstBook = DataSupport.findFirst(Book.class);
查询Book表中的最后一条数据就可以这样写:
Book lastBook = DataSupport.findLast(Book.class);
select() 方法用于指定查询哪几列的数据,对应了SQL当中的select 关键字。比如只查name 和author 这两列的数据,就可以这样写:
List<Book> books = DataSupport.select("name", "author").find(Book.class);
where() 方法用于指定查询的约束条件,对应了SQL当中的where 关键字。比如只查页数大于400的数据,就可以这样写:
List<Book> books = DataSupport.where("pages > ?", "400").find(Book.class);
order() 方法用于指定结果的排序方式,对应了SQL当中的order by 关键字。比如将查询结果按照书价从高到低排序,就可以这样写:
List<Book> books = DataSupport.order("price desc").find(Book.class);
其中desc 表示降序排列,asc 或者不写表示升序排列。
limit() 方法用于指定查询结果的数量,比如只查表中的前3条数据,就可以这样写:
List<Book> books = DataSupport.limit(3).find(Book.class);
offset() 方法用于指定查询结果的偏移量,比如查询表中的第2条、第3条、第4条数据,就可以这样写:
List<Book> books = DataSupport.limit(3).offset(1).find(Book.class);
由于limit(3) 查询到的是前3条数据,这里我们再加上offset(1) 进行一个位置的偏移,就能实现查询第2条、第3条、第4条数据的功能了。
当然,你还可以对这5个方法进行任意的连缀组合,来完成一个比较复杂的查询操作:
List<Book> books = DataSupport.select("name", "author", "pages")
.where("pages > ?", "400")
.order("pages")
.limit(10)
.offset(10)
.find(Book.class);
这段代码就表示,查询Book表中第11~20条满足页数大于400这个条件的name 、author 和pages 这3列数据,并将查询结果按照页数升序排列。
当前,如果你实在有一些特殊需求,上述的API都满足不了你的时候,LitePal仍然支持使用原生的SQL来进行查询: Cursor c = DataSupport.findBySQL("select * from Book where pages > ? and price < ?", "400", "20")
内容提供器(Content Provider) 内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是Android实现跨程序共享数据的标准方式。 不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
■运行时权限 Android开发团队在Android 6.0系统中引用了运行时权限这个功能,从而更好地保护了用户的安全和隐私。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如说一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,但是我应该仍然可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。
当然,并不是所有权限都需要在运行时申请,对于用户来说,不停地授权也很烦琐。Android现在将所有的权限归成了两类,一类是普通权限,一类是危险权限。准确地讲,其实还有第三类特殊权限,不过这种权限使用得很少,因此不在本书的讨论范围之内。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需要用户再去手动操作了,比如在BroadcastTest项目中申请的两个权限就是普通权限。危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能。
但是Android中有一共有上百种权限,我们怎么从中区分哪些是普通权限,哪些是危险权限呢?其实并没有那么难,因为危险权限总共就那么几个,除了危险权限之外,剩余的就都是普通权限了。下表列出了Android中所有的危险权限,一共是9组24个权限。
在MainActivity中的代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{ Manifest.permission.CALL_PHONE }, 1);
} else {
call();
}
}
});
}
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.
PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_
SHORT).show();
}
break;
default:
}
}
}
运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序是不可以擅自做主去执行这些危险操作的。因此,第一步就是要先判断用户是不是已经给过我们授权了,借助的是ContextCompat.checkSelfPermission() 方法。checkSelfPermission() 方法接收两个参数,第一个参数是Context ,这个没什么好说的,第二个参数是具体的权限名,比如打电话的权限名就是Manifest.permission.CALL_PHONE ,然后我们使用方法的返回值和PackageManager.PERMISSION_GRANTED 做比较,相等就说明用户已经授权,不等就表示用户没有授权。
如果已经授权的话就简单了,直接去执行拨打电话的逻辑操作就可以了,这里我们把拨打电话的逻辑封装到了call() 方法当中。如果没有授权的话,则需要调用ActivityCompat.requestPermissions() 方法来向用户申请授权,requestPermissions() 方法接收3个参数,第一个参数要求是Activity的实例,第二个参数是一个String 数组,我们把要申请的权限名放在数组中即可,第三个参数是请求码,只要是唯一值就可以了,这里传入了1。
调用完了requestPermissions() 方法之后,系统会弹出一个权限申请的对话框,然后用户可以选择同意或拒绝我们的权限申请,不论是哪种结果,最终都会回调到onRequestPermissionsResult() 方法中,而授权的结果则会封装在grantResults 参数当中。这里我们只需要判断一下最后的授权结果,如果用户同意的话就调用call() 方法来拨打电话,如果用户拒绝的话我们只能放弃操作,并且弹出一条失败提示。
■ContentResolver的基本用法 对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver() 方法获取到该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,其中insert() 方法用于添加数据,update() 方法用于更新数据,delete() 方法用于删除数据,query() 方法用于查询数据。
不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri 参数代替,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下: content://com.example.app.provider/table1 content://com.example.app.provider/table2
在得到了内容URI字符串之后,我们还需要将它解析成Uri 对象才可以作为参数传入。
Uri uri = Uri.parse("content://com.example.app.provider/table1")
查询数据:
Cursor cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);
查询完成后返回的仍然是一个Cursor 对象,这时我们就可以将数据从Cursor 对象中逐个读取出来了。读取的思路仍然是通过移动游标的位置来遍历Cursor 的所有行,然后再取出每一行中相应列的数据,代码如下所示:
if (cursor != null) {
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
向table1表中添加一条数据,代码如下所示:
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
■创建自己的内容提供器 官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承ContentProvider 的方式来创建一个自己的内容提供器。ContentProvider 类中有6个抽象方法(onCreate(), query(), insert(), update(), delete(), getType()),我们在使用子类继承它的时候,需要将这6个方法全部重写。 1, 新建MyProvider 继承自ContentProvider
public class MyProvider extends ContentProvider {
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, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
// 查询table1表中的所有数据
break;
case TABLE1_ITEM:
// 查询table1表中的单条数据
break;
case TABLE2_DIR:
// 查询table2表中的所有数据
break;
case TABLE2_ITEM:
// 查询table2表中的单条数据
break;
default:
break;
}
...
}
...
}
}
上述代码只是以query() 方法为例做了个示范,其实insert() 、update() 、delete() 这几个方法的实现也是差不多的,它们都会携带Uri 这个参数,然后同样利用UriMatcher的match() 方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。
还有一个方法你会比较陌生,即getType() 方法。它是所有的内容提供器都必须提供的一个方法,用于获取Uri 对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。
1,必须以vnd 开头。
2,如果内容URI以路径结尾,则后接android.cursor.dir/ ,如果内容URI以id结尾,则后接android.cursor.item/ 。
3, 最后接上vnd.
public class MyProvider extends ContentProvider {
...
@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,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
■ JSON数据 解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用谷歌的开源库GSON。另外,一些第三方的开源库如Jackson、FastJSON等也非常不错。
- 使用JSONObject
private void parseJSONWithJSONObject(String jsonData) {
try {
JSONArray jsonArray = new JSONArray(jsonData);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
String id = jsonObject.getString("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 (Exception e) {
e.printStackTrace();
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.google.code.gson:gson:2.7'
}
比如说一段JSON格式的数据如下所示: {"name":"Tom","age":20} 那我们就可以定义一个Person 类,并加入name 和age 这两个字段,然后只需简单地调用如下代码就可以将JSON数据自动解析成一个Person 对象了:
Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);
如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TypeToken将期望解析成的数据类型传入到fromJson() 方法中,
List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>() {}.getType());
服务(Service) 注意:服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。
■Android多线程编程 定义一个线程只需要新建一个类继承自Thread ,然后重写父类的run() 方法。
class MyThread extends Thread {
@Override
public void run() {
// 处理具体的逻辑
}
}
那么该如何启动这个线程呢?其实也很简单,只需要new出MyThread的实例,然后调用它的start() 方法,这样run() 方法中的代码就会在子线程当中运行了
new MyThread().start();
当然,使用继承的方式耦合性有点高,更多的时候我们都会选择使用实现Runnable 接口的方式来定义一个线程,如下所示:
class MyThread implements Runnable {
@Override
public void run() {
// 处理具体的逻辑
}
}
如果使用了这种写法,启动线程的方法也需要进行相应的改变,如下所示:
MyThread myThread = new MyThread();
new Thread(myThread).start();
当然,如果你不想专门再定义一个类去实现Runnable 接口,也可以使用匿名类的方式,这种写法更为常见,如下所示:
new Thread(new Runnable() {
@Override
public void run() {
// 处理具体的逻辑
}
}).start();
■在子线程中更新UI 和许多其他的GUI库一样,Android的UI也是线程不安全的。也就是说,如果想要更新应用程序里的UI元素,则必须在主线程中进行,否则就会出现异常。 对此,Android提供了一套异步消息处理机制,完美地解决了在子线程中进行UI操作的问题。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final int UPDATE_TEXT = 1;
private TextView text;
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;
handler.sendMessage(message); // 将Message对象发送出去
}
}).start();
break;
default:
break;
}
}
}
这里我们先是定义了一个整型常量UPDATE_TEXT ,用于表示更新TextView这个动作。然后新增一个Handler 对象,并重写父类的handleMessage() 方法,在这里对具体的Message进行处理。如果发现Message的what 字段的值等于UPDATE_TEXT ,就将TextView显示的内容改成Nice to meet you。
■ 解析异步消息处理机制 Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。
首先需要在主线程当中创建一个Handler 对象,并重写handleMessage() 方法。然后当子线程中需要进行UI操作时,就创建一个Message 对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage() 方法中。由于Handler是在主线程中创建的,所以此时handleMessage() 方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。
■ 使用AsyncTask 为了更加方便我们在子线程中对UI进行操作,Android还提供了另外一些好用的工具,比如AsyncTask。借助AsyncTask,即使你对异步消息处理机制完全不了解,也可以十分简单地从子线程切换到主线程。当然,AsyncTask背后的实现原理也是基于异步消息处理机制的,只是Android帮我们做了很好的封装而已。
由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下。 Params :在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。 Progress :后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。 Result :当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。 一个最简单的自定义AsyncTask就可以写成如下方式:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
...
}
这里我们把AsyncTask的第一个泛型参数指定为Void ,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer ,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean ,则表示使用布尔型数据来反馈执行结果。
我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下4个。
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
@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("Downloaded " + values[0] + "%");
}
@Override
protected void onPostExecute(Boolean result) {
progressDialog.dismiss(); // 关闭进度对话框
// 在这里提示下载结果
if (result) {
Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show();
}
}
简单来说,使用AsyncTask的诀窍就是,在doInBackground() 方法中执行具体的耗时任务,在onProgressUpdate() 方法中进行UI操作,在onPostExecute() 方法中执行一些任务的收尾工作。
如果想要启动这个任务,只需编写以下代码即可:
new DownloadTask().execute();
我们并不需要去考虑什么异步消息处理机制,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress() 方法,就可以轻松地从子线程切换到UI线程了。
■ 服务的基本用法 1,定义一个服务 (1) 新建一个ServiceTest项目,然后右击com.example.servicetest→New→Service→Service。 其中,Exported 属性表示是否允许除了当前程序之外的其他程序访问这个服务,Enabled 属性表示是否启用这个服务。 (2), 重写了onCreate() 、onStartCommand() 和onDestroy() 这3 个方法,它们是每个服务中最常用到的3个方法了。其中onCreate() 方法会在服务创建的时候调用,onStartCommand() 方法会在每次服务启动的时候调用,onDestroy() 方法会在服务销毁的时候调用。 通常情况下,如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand() 方法里。而当服务销毁时,我们又应该在onDestroy() 方法中去回收那些不再使用的资源。 (3),每一个服务都需要在AndroidManifest.xml文件中进行注册才能生效,不知道你有没有发现,这是Android四大组件共有的特点。不过相信你已经猜到了,智能的Android Studio早已自动帮我们将这一步完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
</service>
</application>
</manifest>
2, 启动和停止服务 MainActivity中的代码
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;
}
}
}
startService() 和stopService() 方法都是定义在Context 类中的,所以我们在活动里可以直接调用这两个方法。注意,这里完全是由活动来决定服务何时停止的,如果没有点击Stop Service按钮,服务就会一直处于运行状态。 那服务有没有什么办法让自已停止下来呢?当然可以,只需要在MyService的任何一个位置调用stopSelf() 方法就能让这个服务停止下来了。
如何才能证实服务已经成功启动或者停止了呢?最简单的方法就是在MyService的几个方法中加入打印日志。
onCreate() 方法 と onStartCommand() 方法 onCreate() 方法是在服务第一次创建的时候调用的,而onStartCommand() 方法则在每次启动服务的时候都会调用,由于刚才我们是第一次点击Start Service按钮,服务此时还未创建过,所以两个方法都会执行,之后如果你再连续多点击几次Start Service按钮,你就会发现只有onStartCommand() 方法可以得到执行了。
3、活动和服务进行通信 onBind() 方法。
绑定与解绑服务的流程简述: 先实现ServiceConnection 重写onServiceConnected() 重写onServiceDisconnected() 再调用bindService()绑定服务 与服务连接时,系统会回调onServiceConnected,要保存IBinder对象,并使用其调用服务。 客户端调用unbindService()解绑服务。 注意,此时不会回调onServiceDisconnected(),这个方法只会在服务crash或killed才会被回调。
服务绑定的应用例子: https://blog.csdn.net/shaw1994/article/details/43854553
■IntentService 类 为了可以简单地创建一个异步的、会自动停止的服务,Android专门提供了一个IntentService 类。
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的问题,因为这个方法已经是在子线程中运行的了。
创建json对应的类 json数据:
"now":{
"tmp":"29",
"cond":{
"txt":"阵雨"
}
}
创建类:
public class Now {
@SerializedName("tmp")
public String temperature;
@SerializedName("cond")
public More more;
public class More {
@SerializedName("txt")
public String info;
}
}
runOnUiThread 在android 中我们一般用 Handler 做主线程 和 子线程 之间的通信 。 现在有了一种更为简洁的写法,就是 Activity 里面的 runOnUiThread( Runnable )方法。 利用Activity.runOnUiThread(Runnable)把更新ui的代码创建在Runnable中,然后在需要更新ui时,把这个Runnable对象传给Activity.runOnUiThread(Runnable)。
Runnable对像就能在ui程序中被调用。如果当前线程是UI线程,那么行动是立即执行。如果当前线程不是UI线程,操作是发布到事件队列的UI线程。
runOnUiThread(new Runnable() {
@Override
public void run() {
if (weather != null && "ok".equals(weather.status)) {
SharedPreferences.Editor editor = PreferenceManager.
getDefaultSharedPreferences(WeatherActivity.this).
edit();
editor.putString("weather", responseText);
editor.apply();
showWeatherInfo(weather);
} else {
Toast.makeText(WeatherActivity.this, "获取天气信息失败",
Toast.LENGTH_SHORT).show();
}
}
});
■Material Design ActionBar已经不被推荐使用了,Toolbar替代了它。
xmlns:app 标识:
在xml布局文件中,我们需要标识xmlns:android指定我们所用到的attribute。
xmlns:android="http://schemas.android.com/apk/res/android"
但由于API升级,有些新添加或者更新的attribute对低版本API无法支持或者效果不一致。所以采用xmlns:app。