rainit2006 / Android-

记录一下Android基本知识
0 stars 0 forks source link

Android入门 #16

Open rainit2006 opened 6 years ago

rainit2006 commented 6 years ago
rainit2006 commented 6 years ago

-《第一行代码》 https://github.com/ZSCDumin/AndroidDevelopmentSummary/blob/master/%E7%AC%AC%E4%B8%80%E8%A1%8C%E4%BB%A3%E7%A0%81%20Android%20%E7%AC%AC2%E7%89%88-%E9%83%AD%E9%9C%96-%E4%BA%BA%E9%82%AE-2016.12-P580.pdf

rainit2006 commented 6 years ago

注意事项:

rainit2006 commented 6 years ago

■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();
rainit2006 commented 6 years ago

■Intent インテントには「意図」「目的」という意味があります。主にアクティビティを起動する際のパラメータに使われます。

显式Intent:即直接指定需要打开的activity对应的类。 隐式Intent:隐式不明确指定启动哪个Activity,而是设置Action、Data、Category,让系统来筛选出合适的Activity。筛选是根据所有的来筛选。 https://blog.csdn.net/xiao__gui/article/details/11392987

一般的なインテント:アラーム、カレンダ、カメラ、Webブラウザ。。。 https://developer.android.com/guide/components/intents-common?hl=ja

数据传递: 渡すときはputExtra()のメソッドのみだが取り出す際は int型ならgetIntExtra(""); String型ならgetStringExtra(""); のように型に対応したメソッドを呼び出す。

startActiivityForResult(requestcode,intnet): 当子窗体关闭时,父窗体会执行onActivityResult()方法,并可以获取子窗体的返回值.

  1. 首先我们要在第一个Activity调用startActivityForResult这个方法,代码如下:
    public void onClick(View v) {
                //得到新打开Activity关闭后返回的数据
                //第二个参数为请求码,可以根据业务需求自己编号
                startActivityForResult(new Intent(MainActivity.this, OtherActivity.class), 1);
            }
        });
  2. 在第二个Activity中使用setResult设置要返回的值:
    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();
            }
        });
  3. 当第二个Activity关闭时,返回第一个Activity,在第一个Activity中重写onActivityResult方法,数据可以从data中取出: 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

image

rainit2006 commented 6 years ago

■ 生命周期 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() 就可以。 image

■启动活动的最佳写法

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");
    }
});
rainit2006 commented 6 years ago

■引入布局 新建一个布局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时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。

rainit2006 commented 6 years ago

■ ListView的使用


- 定制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 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, 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(); } }); }

rainit2006 commented 6 years ago

■ RecyclerView的使用 Android提供了一个更强大的滚动控件——RecyclerView。它可以说是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。目前Android官方更加推荐使用RecyclerView。

添加完之后记得要点击一下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很难或者根本无法实现的效果在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文件.

我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容会被放置的区域。 image

然后把该图片作为view item的背景图 image

效果: image 这样当图片需要拉伸的时候,就可以只拉伸指定的区域,程序在外观上也有了很大的改进。

rainit2006 commented 6 years ago

■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库也一起引入进来。

完成,效果图: image

另外当碎片中需要使用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() 方法,最终将碎片销毁掉。

例:在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity_main.xml布局。 这就意味着,当程序运行在屏幕宽度大于600dp的设备上时,会加载layout-sw600dp/activity_main布局,当程序运行在屏幕宽度小于600dp的设备上时,则仍然加载默认的layout/activity_main布局。

rainit2006 commented 6 years ago

广播机制 ■ Android中的广播主要可以分为两种类型:标准广播和有序广播。 标准广播 (Normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。 有序广播 (Ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。

Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,时间或时区发生改变也会发出一条广播,等等。如果想要接收到这些广播,就需要使用广播接收器

■ 接收系统广播

  1. 动态注册监听网络变化 注册广播的方式一般有两种,在代码中注册和在AndroidManifest.xml中注册,其中前者也被称为动态注册,后者也被称为静态注册。

那么该如何创建一个广播接收器呢?其实只需要新建一个类,让它继承自BroadcastReceiver ,并重写父类的onReceive() 方法就行了。这样当有广播到来时,onReceive() 方法就会得到执行,具体的逻辑就可以在这个方法中处理。

在onReceive() 方法中,首先通过getSystemService() 方法得到了ConnectivityManager 的实例,这是一个系统服务类,专门用于管理网络连接的。然后调用它的getActiveNetworkInfo() 方法可以得到NetworkInfo 的实例,接着调用NetworkInfo 的isAvailable() 方法,就可以判断出当前是否有网络了,最后我们还是通过Toast的方式对用户进行提示。

还要注意,需要在AndroidManifest.xml文件里面加入如下权限以访问系统网络状态

  • 静态注册 动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大的优势,但是它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在onCreate() 方法中的。那么有没有什么办法可以让程序在未启动的情况下就能接收到广播呢?这就需要使用静态注册的方式了。

可以使用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>

可以看到, 标签内出现了一个新的标签 ,所有静态的广播接收器都是在这里进行注册的。它的用法其实和 标签非常相似,也是通过android:name 来指定具体注册哪一个广播接收器,而enabled 和exported 属性则是根据我们刚才勾选的状态自动生成的。

需要注意的是,不要在onReceive() 方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive() 方法运行了较长时间而没有结束时,程序就会报错。因此广播接收器更多的是扮演一种打开程序其他组件的角色,比如创建一条状态栏通知,或者启动一个服务等。

■ 发送自定义广播

  • 发送标准广播

    1. 新建一个MyBroadcastReceiver
      
      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 这条广播的广播接收器就会收到消息。此时发出去的广播就是一条标准广播。
  • 发送有序广播

    1. 发送有序广播只需要改动一行代码,即将sendBroadcast() 方法改成sendOrderedBroadcast() 方法。sendOrderedBroadcast() 方法接收两个参数,第一个参数仍然是Intent ,第二个参数是一个与权限相关的字符串,这里传入null 就行了。
  1. 那么该如何设定广播接收器的先后顺序呢?当然是在注册的时候进行设定的了,修改AndroidManifest.xml中的代码, 通过android:priority 属性给广播接收器设置了优先级,优先级比较高的广播接收器就可以先收到广播。这里将MyBroadcastReceiver的优先级设成了100,以保证它一定会在AnotherBroadcastReceiver之前收到广播。
  2. 如果在onReceive() 方法中调用了abortBroadcast() 方法,就表示将这条广播截断,后面的广播接收器将无法再接收到这条广播。

■使用本地广播 为了能够简单地解决广播的安全性问题,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();
        }

另外还有一点需要说明,本地广播是无法通过静态注册的方式来接收的。其实这也完全可以理解,因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,而发送本地广播时,我们的程序肯定是已经启动了,因此也完全不需要使用静态注册的功能。

rainit2006 commented 6 years ago

数据持久化 Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储、SharedPreferences存储以及数据库存储。当然,除了这3种方式之外,你还可以将数据保存在手机的SD卡中. ■文件存储

核心技术就是Context 类中提供的openFileInput() 和openFileOutput() 方法,之后就是利用Java的各种流来进行读写操作。

Context 类中提供了一个openFileOutput() 方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data//files/目录下的。第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。

注意: 其实文件的操作模式本来还有另外两种: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();
        }
    }
}
处理思路非常简单,在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 ("

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() 方法。
rainit2006 commented 6 years ago

■使用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来升级数据库非常非常简单,你完全不用思考任何的逻辑,只需要改你想改的任何内容,然后将版本号加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来说,对象是否已存储就是根据调用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() 方法中没有指定约束条件,因此更新操作对所有数据都生效了。

当前,如果你实在有一些特殊需求,上述的API都满足不了你的时候,LitePal仍然支持使用原生的SQL来进行查询: Cursor c = DataSupport.findBySQL("select * from Book where pages > ? and price < ?", "400", "20")

rainit2006 commented 6 years ago

内容提供器(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);

image

查询完成后返回的仍然是一个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.. 。 所以,对于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() 方法中的逻辑,代码如下所示:

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,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。

rainit2006 commented 6 years ago

■ 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();
        }
    }

比如说一段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());

rainit2006 commented 6 years ago

服务(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操作了。 image

■ 使用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的问题,因为这个方法已经是在子线程中运行的了。

rainit2006 commented 6 years ago

创建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;

    }
}
rainit2006 commented 6 years ago

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();
                            }
                        }
                    });
rainit2006 commented 6 years ago

■Material Design ActionBar已经不被推荐使用了,Toolbar替代了它。

xmlns:app 标识: 在xml布局文件中,我们需要标识xmlns:android指定我们所用到的attribute。 xmlns:android="http://schemas.android.com/apk/res/android" 但由于API升级,有些新添加或者更新的attribute对低版本API无法支持或者效果不一致。所以采用xmlns:app。