生存是唯一的长路


  • 资料来源如下
  • 第一行代码(第二版)

Activity基础

Activity定义

  • Activity 是Android四大组件之一,用户可与其提供的屏幕进行交互,以执行操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口可以充满屏幕,也可浮动。应用通常由多个彼此联系的 Activity 组成。应用中的某个 Activity 为“主”Activity,即首次启动应用时的Activity。

创建Activity

  • 新建FirstAtivity继承自AppCompatActivity

    1
    2
    3
    4
    5
    public class FirstActivity extends AppCompatActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    } }
  • 创建加载布局
    1.jpg

  • 切换到first-layout

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
    android:id="@+id/button_1"
    android:text="button_1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
    </LinearLayout>
  • 在FirestActivity中加载layout
    在onCreate中加入

    1
    setContentView(R.layout.first_layout);
  • 在AndroidMainfest文件中注册。

    1
    2
    3
    4
    5
    6
    7
    8
    <activity
    android:name=".FirstActivity"
    android:label="This is FirstActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    </activity>
  • 在模拟器中启动apk
    5ea429.jpg

使用Toast

Toast google官方说明

  • 推送一个短小信息推送给用户
    如图(摘自android developer)
    toast.png

  • 使用方法

    1
    2
    Toast.makeText(context, text, duration).show();

    举例

    1
    2
    Toast.makeText(FirstActivity.this, "you clicked button 1", Toast.LENGTH_SHORT).show();
    /* activity.this 消息内容 显示时间设置 */

使用Menu

  • 创建菜单

    • 在res下新疆menu文件夹,右击menu文件夹—new—Menu resource file,创建main的菜单文件。
    • main.xml中添加如下代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
       <?xml version="1.0" encoding="utf-8"?>
      <menu xmlns:android="http://schemas.android.com/apk/res/android">
      <item
      android:id="@+id/add_item"
      android:title="Add"/>
      <item
      android:id="@+id/remove_item"
      android:title="Remove"/>
      </menu>
      这里创建了两个菜单项 Add和Remove
    • 在FirestActivity中重写 onCreateOptionsMenu()方法(快捷键 Ctrl+O)
      1
      2
      3
      4
      5
        @Override
      public boolean onCreateOptionsMenu(Menu menu) {
      getMenuInflater().inflate(R.menu.main, menu);
      return true;
      }
      getMenuInflater()可以得到MenuInflater对象,再调用.inflate就可以创建菜单。.inflat接受俩个参数,一是资源文件名,二是菜单项添加至那个对象中。onCreateOptionsMenu方法中返回true表示创建菜单并显示。
    • 效果如下。
      Screenshot_1481113497.png
  • 创建菜单点击响应事件

    • 重写onOptionsItemSelected方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
      switch (item.getItemId()) {
      case R.id.add_item:
      Toast.makeText(this, "Add", Toast.LENGTH_SHORT).show();
      break;
      case R.id.remove_item:
      Toast.makeText(this, "Remove", Toast.LENGTH_SHORT).show();
      break;
      default:
      }
      return true;
      }
      通过item.getItemId()判断点击选项,弹出不同的Toast

向一个activity 并传递字符串

  • 构建一个Intent
    Intent intent = new Intent(this, DisplayMessageActivity.class);
    构造方法有两个参数:
    Context 是第一个参数,这里使用了this是因为Activity是Context的子类。
    Class 类是系统将要分发的APP组件,在这里,这个Activity将会被启动。

  • public void sendMessage(View view) {
        // 创建一个新的intent对象,绑定DisplayMessageActivity
        Intent intent = new Intent(this, DisplayMessageActivity.class);
        //创建一个editText对象,绑定xml中editText
        EditText editText = (EditText) findViewById(R.id.edit_message);
        //获取editText中输入文字,转成字符串
        String message = editText.getText().toString();
        //一个Intent对象可以携带被称为extras的键值对。
        // putExtra()方法将键放在第一个参数中,将值放在第二个参数中。
        intent.putExtra(EXTRA_MESSAGE, message);
        //启动intent对应Activity
        startActivity(intent);
        }
    <!--code9-->
    
    

返回数据给上一个Activity

  • 构建Intent使用startActivityForResult()方法传入请求码,启动下一个Activity
    在下一个Activity中构建Intent,intent.putExtra存入键值-key,调用setResult()方法,传入finish()结束掉Activity
    重写第一个Activity中的onActivityResult()方法,调用.getStringExtra取出key对应键值。

  • startActivityForResult(Intent, int Bundle) Intent与单纯启动Activity的Intent相同,第二个是请求码,下一级 回调提供相同的请求码,以便您应用可以正确识别结果。

    1
    2
    Intent intent = new Intent(FirestActivity.this,ScendActivity.class);
    startActivityForResult(intent,1);
  • setResult()方法,第一个参数向上级方法处理结果,一般使用RESULT_OK或RESULT_CANCELED,第二个参数 对应Intent

    1
    2
    3
    4
    5
    6
    7
    //新建显示Intent
    Intent intent = new Intent();
    //存入key-键值
    intent.putExtra("data_return","Hello Firest");
    setResult(RESULT_OK,intent);
    //结束Activity
    finish();
  • onActivityResult()三个参数.第一个startActivityForResult() 传递的请求代码。第二个 Activity 指定的结果代码。成功是 RESULT_OK;失败,则是 RESULT_CANCELED。第三个是传送结果数据的 Intent。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     @Override
    protected void onActivityResult(int requestCode,int resultCode,Intent data){
    //选择不同请求码对应处理逻辑
    switch(requestCode){
    case 1:
    //处理结果时候ok
    if(resultCode == RESULT_OK){
    //取出数据
    String returnedData = data.getStringExtra("data_return");
    Log.d("FirstActivity", returnedData);
    }
    }
    }

Activity生命周期

basic-lifecycle.png

  • Activity 3种状态
    • Resumed/继续
      Activity 处于前台,且用户可以与其交互
    • Paused/暂停
      Activity 被在前台中处于另一个 Activity—部分阻挡。 暂停的 Activity 不会接收用户输入并且无法执行任何代码。
    • Stopped/停止
      Activity完全隐藏,对用户完全不可见.当停止时,activity的所有状态信息比如成员变量都会被保留,但是不能再执行任何代码

启动一个Activity

  • onCreate方法
  • 主Activity:
    • 用户点击app图标,启动主Activity
    • 在AndroidManifest.xml中声明
      1
      2
      3
      4
      <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
  • 通过调用onCreate方法创建一个Activity实例,onCreate方法在Activity的整个生命周期中只执行一次。
  • 之后系统会很快的调用onStart和onResume方法,Activity进入Resumed/继续模式。直到被其他activity覆盖/屏幕关闭。

销毁Activity

  • onDestory方法
    大多数的APP不需要实现这个方法,因为本地类引用会随着Activity一起总结,不过Activity的清理工作应该放在onPause下或者onStop。
  • Note:
    在所有的情况下系统调用onDestory方法之后已经调用过onPause方法与onStop方法,不过有一个例外情况:你在onCreate方法中调用了finish方法。在一些例子中,当你的Activity临时决定要启动另一个Activity,你可能要在onCreate方法内调用finish方法来销毁这个Activity,在这种情况下,系统会立即调用onDestory方法,而不会调用其它任何生命周期方法。

暂停Activity

  • onPause()方法
  • Activity被其他Activity覆盖/失去用户焦点,系统调用onPause()方法,Activity 进入暂停状态。
    • note:android7.0及以上版本加入了多窗口模式,当Activity失去用户焦点时,可能处于多窗口模式。
  • onPause() 常用回调:
    • 检查 Activity 是否可见。不可见则停止可能消耗 CPU 的操作
    • 提交未保存的更改,仅保存用户离开时希望永久性保存此类更改(比如电子邮件草稿)。
    • 释放系统资源,GPS/Camer等
    • 示例 (释放Camer)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public void onPause() {
      super.onPause(); // Always call the superclass method first

      // Release the Camera because we don't need it when paused
      // and other activities might need to use it.
      if (mCamera != null) {
      mCamera.release();
      mCamera = null;
      }
    • 注意事项:
      • 在onPause()一般不执行永久性存储用户更改,不执行 CPU 密集型工作,这些工作一般放在onStop() 。

继续 Activity

  • onResume() 方法
  • 暂停状态回到继续状态,Activity第一次启动时也会调用这个方法。
  • onResume() 以初始化在 onPause() 期间释放的组件。
  • 示例(重新获取Camera)
    1
    2
    3
    4
    5
    6
    7
    public void onResume() {
    super.onResume(); // Always call the superclass method first

    // Get the Camera instance as the activity achieves full user focus
    if (mCamera == null) {
    initializeCamera(); // Local method to handle camera init
    }

停止 Activity

  • note:
    大多数相对简单的 Activity 而言,系统在 Activity 停止时会将Activity 实例保留在系统内存中,无需实现 onStop() 和 onRestart() 或甚至onStart() 方法。可能只需使用 onPause() 暂停正在进行的操作,并从系统资源断开连接。

  • onStop() 方法

  • 场景:

    • 用户在最近应用切换到另一个应用
    • 应用中执行开始新 Activity 的操作
    • Activity使用时,接打电话
  • 调用时,Activity不再可见,释放几乎所有用户不使用时不需要的资源。如果系统内存紧张,则可能销毁内存中的Acitivity实例。

  • onStop() 方法调用后,Activity不再可见,极端情况下,系统可能会仅终止应用进程,而不调用 onDestroy() ,因此需要使用 onStop() 释放几乎所有用户不使用时不需要的资源。

  • 尽管 onPause() 方法在 onStop()之前调用,在onStop() 执行更大、占用更多 CPU 的关闭操作,比如向数据库写入信息

  • 示例(草稿笔记内容保存在永久存储)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      protected void onStop() {
    super.onStop(); // Always call the superclass method first

    // Save the note's current draft, because the activity is stopping
    // and we want to be sure the current note progress isn't lost.
    ContentValues values = new ContentValues();
    values.put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText());
    values.put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle());

    getContentResolver().update(
    mUri, // The URI for the note to update.
    values, // The map of column names and new values to apply to them.
    null, // No SELECT criteria are used.
    null // No WHERE columns are used.
    );
    }

启动/重启 Activity

  • onStart() 方法
  • Activity 停止转换为继续状态时,系统回调onRestart() 方法+ onStart() 方法.onStop() 方法清理了所有 Activity 的资源,重启 Activity 需要重新实例化它们。同时 Activity 初次创建时重新实例化它们。 出于此,经常使用 onStart() 方法作为 onStop() 方法的对应
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     @Override
    protected void onStart() {
    super.onStart(); // Always call the superclass method first

    // The activity is either being restarted or started for the first time
    // so this is where we should make sure that GPS is enabled
    LocationManager locationManager =
    (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);

    if (!gpsEnabled) {
    // Create a dialog here that requests the user to enable GPS, and use an intent
    // with the android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS action
    // to take the user to the Settings screen to enable GPS when they click "OK"
    }
    }

    @Override
    protected void onRestart() {
    super.onRestart(); // Always call the superclass method first

    // Activity being restarted from stopped state
    }

保存 Activity 状态

  • onSaveInstanceState()方法

  • 默认情况下,Activity 实例被销毁时系统会使用 Bundle 实例状态保存 Activity 布局中有关每个 View 对象的信息。在Activity 重建时,布局状态便自动恢复先前的状态。

  • 默认实现保存有关 Activity 视图层次的状态信息,例如 EditText 小部件中的文本或ListView 的滚动位置

  • 要恢复的更多信息,需要重写 onSaveInstanceState()方法,将键值对添加至 Bundle 对象

  • basic-lifecycle-savestate.png

  • note:
    旋转屏幕时,Activity 将被销毁并重新创建。原因:方向更改时可能需要时加载备用资源(比如布局)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static final String STATE_SCORE = "playerScore";
    static final String STATE_LEVEL = "playerLevel";
    ...

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current game state
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
    }

恢复 Activity

  • onCreate() 和 onRestoreInstanceState() 回调方法均接收包含实例状态信息的相同 Bundle
  • onCreate() 方法
  • 调用onCreate() 方法需要区分是创建 Activity 的新实例还是恢复先前的实例,判断 Bundle 是否为 null
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // Always call the superclass first

    // Check whether we're recreating a previously destroyed instance
    if (savedInstanceState != null) {
    // Restore value of members from saved state
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    } else {
    // Probably initialize members with default values for a new instance
    }
    ...
    }
  • onRestoreInstanceState()方法
  • 只需要恢复的已保存的状态
  • 示例
1
2
3
4
5
6
7
8
 public void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState);

// Restore state members from saved instance
mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
}

重启Activity其他选择

  • 官方说明
  • 重启应用并恢复大量数据不仅成本高昂,而且会留下糟糕的使用体验,有两个其他选择

在配置变更期间保留对象

  • Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担
  • 当 Android 系统因配置变更而关闭 Activity 时,不会销毁已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。

Activity最佳实践

知晓当前运行的活动

  • 自定义BaseActivity继承自AppCompatActivity
    重写onCreate方法,打印当前运行的Activity名
    app类所有Activity改为继承BaseActivity

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //打印当前类名
    Log.d("BaseActivity", getClass().getSimpleName());
    }
  • 效果如下
    ScreenShot_20161208172740.png

随时退出程序

  • 新建ActivityCollector类作为活动管理器,添加/删除Activity登记,在BaseActivity中添加对应代码。
  • 代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     public class ActivityCollector {
    //新建Activity的列表对象
    public static List<Activity> activities = new ArrayList<>();
    //添加Activity进入列表
    public static void addActivity(Activity activity) {
    activities.add(activity);
    }
    //在列表中移除Activity
    public static void removeActivity(Activity activity) {
    activities.remove(activity);
    }
    //for循环列表 结束所有Activity
    public static void finishAll() {
    for (Activity activity : activities) {
    if (!activity.isFinishing()) {
    activity.finish();
    }
    }
    }
    }
  • BaseActivity
    在onCreate()方法中添加
    1
    2
    //Activity管理器中添加新的活动
    ActivityCollector.addActivity(this);
    重写onDestroy()方法
    1
    2
    3
    4
    5
    6
    @Override
    protected void onDestroy(){
    super.onDestroy();
    //Activity管理器中移除活动
    ActivityCollector.removeActivity(this);
    }
  • 在任何地方调用finishAll()方法即可。

启动活动的最近写法

  • 启动下一个Activity并传递数据模块化
  • 在SecondActivity内增加actionStart()方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     //调用的Activity,数据1,数据2
    public static void actionStart(Context context, String data1, String data2){
    //新建Intent,绑定SecondActivity
    Intent intent = new Intent(context,ScendActivity.class);
    //存入data1,data2
    intent.putExtra("param1",data1);
    intent.putExtra("param2",data2);
    //启动Activity
    context.startActivity(intent);
    }
  • 启动SecondActivity方式
    ScendActivity.actionStart(FirestActivity.this,"data1","data2");


资料来源如下

  • 网络

编程环境

  • Android Studio 2.2.3

导语

  • 转眼刚用上手的MVP又不符合需求了,还是总结一下,继续MVVM吧。

  • 主要内容是转载,搭建小工程应该足够了。
    Android mvp 架构的自述
    如何更高效的使用MVP以及官方MVP架构解析

  • 框架开源在GitHub 地址点我直达

  • 好,我们开始吧!


MVC MVP MVVM

  • 省略200行,详情看Android App的设计架构:MVC,MVP,MVVM与架构经验谈
  • 再累赘属于掉书袋了,作者写的是很用心,恩,MVVM也有。

框架详解

  • 整个框架截图
    ScreenShot_20170509215208.png

  • 整体层次比较明确了,BaseClass 里存放基类,Model层存放数据存储有关类、Presenter层存放逻辑代码、View层存放Activity、frament等。

  • 这个框架是由MVP在Android中应用存在的问题而搭建的,基类中代码也由此而来。

问题

  • presenter一直持有Activity对象导致的内存泄漏问题

    • 使用mvp的时候,presenter会持有view,如果presenter有后台异步的长时间的动作,比如网络请求,这时如果返回退出了Activity,后台异步的动作不会立即停止,这里就会有内存泄漏的隐患。
    • 需要一个销毁view的方法,这里是在基类中完成的。
  • presenter绑定和解绑繁琐

    • 一般一个presenter对应一个Activity,一般应用内存在多个Actiivity,绑定与解绑相当繁琐。
    • 统一在 Basepresenter 与 BaseActivity中进行,同时解决内存泄漏问题,还有其他常用内容一并添加。
  • presenter 与 Model 的通信问题。

    • presenter已经与View层 强耦合了,框架中需要解耦presenter 与 Model ,使用异步通信。Handle等都太复杂了。。。
    • 开始没有什么好的解决办法,直到遇到了EventBus,当然EventBus也不是万能解决方案,跨进程,还是要另辟蹊径,不过EventBus足够自己写着完了。还有Rxjava,恩,还在玩着,没搞太懂。

代码

  • BasePresenter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     public abstract class BasePresenter<Viewinterface> {
    //传入泛型
    public Viewinterface mView;
    //绑定
    public void attach(Viewinterface mView) {
    this.mView = mView;
    }
    //解绑,防止view为空是内存泄漏
    public void dettach() {
    mView = null;
    }
    }
  • BaseActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
     public abstract class BaseActivity <Viewinterface,mPresenter extends BasePresenter<Viewinterface>>extends AppCompatActivity {
    //获取Presenter对象
    public mPresenter mpresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Presenter实例化
    mpresenter = initPresenter();
    //打印当前activity
    LogUtil.d("Activity", getClass().getSimpleName()+"onCreate");
    }

    @Override
    protected void onResume() {
    //重新刷新时重新绑定view
    mpresenter.attach((Viewinterface) this);
    super.onResume();
    LogUtil.d("Activity", getClass().getSimpleName()+"onResume");
    }

    @Override
    protected void onDestroy() {
    //解绑presenter持有的view
    mpresenter.dettach();
    super.onDestroy();
    LogUtil.d("Activity", getClass().getSimpleName()+"onDestroy");
    }


    public abstract mPresenter initPresenter();
    }
  • Presenter

    1
    2
     public interface Presenter {
    }
  • mPresenter

    1
    2
    3
    4
    5
    6
    7
    8
     public class mPresenter extends BasePresenter<Viewif> implements Presenter {
    private Model mModel;

    public mPresenter(Viewif view) {
    this.attach(view);
    this.mModel = new mModel();
    }
    }
  • BaseView

1
2
3
 public interface BaseView<T>  {

}
  • Viewif

    1
    2
    3
     public interface Viewif extends BaseView {

    }
  • MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     public class MainActivity extends BaseActivity<Viewif, mPresenter> implements Viewif{

    Presenter presenter;
    //Presenter初始化
    public mPresenter initPresenter() {
    return new mPresenter(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //presenter初始化
    presenter = mpresenter;
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }
    }
  • LogUtil

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
     public class LogUtil {
    public static final int VERBOSE = 1;//啰嗦,等级最低的
    public static final int DEBUG = 2;//调试
    public static final int INFO = 3;//信息
    public static final int WARN = 4;//警告
    public static final int ERROR = 5;//错误
    public static final int NOTHING = 6;//什么也不打印出来
    public static final int level = VERBOSE;//LEVEL:标准

    public static void v(String tag, String msg) {
    if (level <= VERBOSE) {//如果大于或者等于定义的标准就打印出来
    Log.v(tag, msg);
    }
    }

    public static void d(String tag, String msg) {
    if (level <= DEBUG) {
    Log.d(tag, msg);
    }
    }

    public static void i(String tag, String msg) {
    if (level <= INFO) {
    Log.i(tag, msg);
    }
    }

    public static void w(String tag, String msg) {
    if (level <= WARN) {
    Log.w(tag, msg);
    }
    }

    public static void e(String tag, String msg) {
    if (level <= ERROR) {
    Log.e(tag, msg);
    }
    }
    }


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

导语

  • 毕设中的蓝牙部分,记录以供复习

概述

最终效果

  • Android应用 与 Hc-05 蓝牙模块连接,单片机与 Android 端 可以通过串口正常收发数据.

  • 预留足够灵活的接口, Android 应用中可以在任意位置获取到蓝牙数据

  • Activity切换时,蓝牙连接不断开.

用到的开源库/知识点资料 简介/来源

  • 蓝牙:
    Android API 指南

https://developer.android.com/guide/topics/connectivity/bluetooth.html

  • EventBus:
    EventBus-GitHub 是一个Android端优化的publish/subscribe消息总线,用来替代 Intent 、 Handler 、 Broadcast 等在 Actvity 、 Fragment 、 Service 等组件之间传递信息 . EventBus 可以传递一个完整的对象,简单高效, 注意 EventBus 只能在多线程之间传递消息,无法在不同进程之间传递消息 ,EventBus 3.0 以后进一步简化了传递方式,真的是很值得学习的一个开源库!

    参考资料如下:

    EventBus 3.0初探: 入门使用及其使用 完全解析

    EventBus3(3.0.0)源码解析


基础部分

  • 蓝牙 与 EventBus 基础部分

BlueTooth


  • 备注 : 这里使用的是 传统蓝牙 即 蓝牙4.0以前版本, 而不是 蓝牙4.0 ( ble低功耗蓝牙)及以后版本

  • Android 对 Bluetooth 做了很好的封装,我们可以比较轻松的在 Android Bluetooth API 上进行开发.
  • Android中所有蓝牙API均来自 android.bluetooth 包

BlueTooth基础

  • BluetoothAdapter 本地蓝牙适配器
    BluetoothAdapter 是所有蓝牙交互的入口点, 在初始化蓝牙及蓝牙配对阶段使用
    1.发现其他蓝牙设备
    2.查询绑定(配对)设备的列表
    3.使用已知的 MAC 地址实例化 BluetoothDevice
    4.创建 BluetoothServerSocket 侦听来自其他设备的通信。

  • BluetoothDevice 远程蓝牙设备
    含有该设备的信息,例如设备的名称、地址、类和绑定状态等。

  • BluetoothSocket 蓝牙套接字接口(与 TCP Socket 相似)
    与 BluetoothDevice 配合建立远程连接,允许应用通过 InputStream 和 OutputStream 与其他蓝牙设备交换数据.

初始化蓝牙

  • 声明蓝牙权限
    Android 应用使用蓝牙前都需要声明蓝牙权限
    一般只需要 BLUETOOTH 权限即可,但是考虑到之后有更多的需求,需要更改系统蓝牙设置,由此需要 BLUETOOTH_ADMIN 权限_也一起声明

    1
    2
    3
    4
    5
    <manifest ... >
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    ...
    </manifest>
  • 蓝牙设备可用性 获取 BluetoothAdapter
    获取 BluetoothAdapter,需要静态 getDefaultAdapter() 方法。getDefaultAdapter() 会返回一个表示设备自身的蓝牙适配器的 BluetoothAdapter 设备不支持蓝牙 则返回 null

    1
    2
    3
    4
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
    // 蓝牙不可用操作
    }
  • 开启蓝牙
    BluetoothAdapter 的 .isEnabled() 可以判断系统蓝牙是否开启.
    没有开启时 startActivityForResult() 发送包含 ACTION_REQUEST_ENABLE 的 Intent 请求系统开启蓝牙,并在 onActivityResult() 返回的数据中 RESULT_CANCELED 表示开启失败 RESULT_OK 表示开启成功,这里我们只检测开启失败情况,并提示用户

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED) {
    Toast.makeText(MyApplication.getContext(),R.string.Bluetooth_openfail, Toast.LENGTH_SHORT).show();
    }
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
    //设备不支持蓝牙时处理
    }
    }
  • 查询已配对的设备
    调用 getBondedDevices(),返回已配对设备的一组 BluetoothDevice.之后使用for循环遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
    if (pairedDevices.size() > 0) {
    //重新加载bluetoothDeviceList
    bluetoothDeviceList.clear();
    //BluetoothDevice列表循环
    for (BluetoothDevice device : pairedDevices) {
    bluetoothDeviceList.add(device);
    }
    }
  • 扫描设备
    查找设备是非常耗费系统资源的事项,需要安排在子线程中执行,这里没有用到,由需要请参考Google官方蓝牙教程

  • 连接设备
    蓝牙分主从机,链接为服务器/客户端。这里使用的是连接为客户端。

    • 首先获取远程设备的 BluetoothDevice 对象,即在选择设备阶段的BluetoothDevice
    • 调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket,UUID通用唯一识别码 在蓝牙中具体是什么没有很好的解释。这里的值取的是
      "00001101-0000-1000-8000-00805F9B34FB"
    • 调用 connect() 发起连接,阻塞调用,需要在子线程中执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    //蓝牙连接子线程
    private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;

    //解析函数
    ConnectThread(BluetoothDevice bluetoothDevice) {
    // 使用一个中间变量 tmp
    // mmSocket 类型是 final

    BluetoothSocket tmp = null;
    // 获取 BluetoothSocket
    try {
    // MY_UUID is the app's UUID string, also used by the server code
    tmp = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MyApplication.MY_UUID));
    } catch (IOException ignored) {
    }
    //赋值给 mmSocket
    mmSocket = tmp;
    }

    public void run() {
    // 关闭蓝牙扫描
    MyApplication.getBluetoothAdapter().cancelDiscovery();
    try {
    // 通过 socket 连接到设备. connect() 会一直执行直到成功连接或者抛出异常
    mmSocket.connect();
    } catch (IOException connectException) {
    //无法连接到蓝牙,关闭连接并退出
    try {
    mmSocket.close();
    }
    //没有正常关闭
    catch (IOException ignored) {
    }
    return;
    }
    // Do work to manage the connection (in a separate thread)
    mConnectedThread = new ConnectedThread(mmSocket);
    mConnectedThread.start();
    }

    /**
    * 关闭蓝牙连接
    */
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException ignored) {
    }
    }
    }
  • 管理连接
    获取BluetoothSocket
    获取 InputStream 和 OutputStream, getInputStream() 和 getOutputStream() 来处理数据传输。read(byte[]) 和 write(byte[]) 读取数据并写入到流式传输。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    private class ConnectedThread extends Thread {
    //BluetoothSocket
    private final BluetoothSocket mmSocket;
    //输入流
    private final InputStream mmInStream;
    //输出流
    private final OutputStream mmOutStream;

    ConnectedThread(BluetoothSocket socket) {
    //传入BluetoothSocket,实例化mmSocket
    mmSocket = socket;
    //输入/输出流 中间变量
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    // 输入输出流实例化
    try {
    tmpIn = socket.getInputStream();
    tmpOut = socket.getOutputStream();
    } catch (IOException e) {
    e.printStackTrace();
    }
    mmInStream = tmpIn;
    mmOutStream = tmpOut;
    }

    public void run() {
    byte[] buffer = new byte[1024];
    int bytes;
    // 连接成功时
    while (true) {
    try {
    // 在InputStream读数据
    bytes = mmInStream.read();
    //发送数据
    Events.bluetooth_Recycle bluetooth_recycle = new Events.bluetooth_Recycle();
    bluetooth_recycle.s = bytes;
    bluetooth_recycle.bytes = String.valueOf((char) bytes);
    EventBus.getDefault().post(bluetooth_recycle);
    } catch (IOException e) {
    break;
    }
    }
    }

    //写方法
    void write(byte[] bytes) {
    try {
    mmOutStream.write(bytes);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //关闭流连接
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException e) {
    }
    }
    }

EventBus

  • 直接放链接了

    http://www.jianshu.com/p/a040955194fc
    http://www.jianshu.com/p/acfe78296bb5
    http://www.ff50.net/view/40565212977623506063.html

正文

  • 蓝牙连接子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
     private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;

    //解析函数
    ConnectThread(BluetoothDevice bluetoothDevice) {
    // 使用一个中间变量 tmp
    // mmSocket 类型是 final

    BluetoothSocket tmp = null;
    // 获取 BluetoothSocket
    try {
    // MY_UUID is the app's UUID string, also used by the server code
    tmp = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MyApplication.MY_UUID));
    } catch (IOException ignored) {
    }
    //赋值给 mmSocket
    mmSocket = tmp;
    }

    public void run() {
    // 关闭蓝牙扫描
    MyApplication.getBluetoothAdapter().cancelDiscovery();
    try {
    // 通过 socket 连接到设备. connect() 会一直执行直到成功连接或者抛出异常
    mmSocket.connect();
    } catch (IOException connectException) {
    //无法连接到蓝牙,关闭连接并退出
    try {
    mmSocket.close();
    }
    //没有正常关闭
    catch (IOException ignored) {
    }
    return;
    }
    // Do work to manage the connection (in a separate thread)
    mConnectedThread = new ConnectedThread(mmSocket);
    mConnectedThread.start();
    }

    /**
    * 关闭蓝牙连接
    */
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException ignored) {
    }
    }
    }
  • 管理连接子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
      //管理连接子线程
    private class ConnectedThread extends Thread {
    //BluetoothSocket
    private final BluetoothSocket mmSocket;
    //输入流
    private final InputStream mmInStream;
    //输出流
    private final OutputStream mmOutStream;

    ConnectedThread(BluetoothSocket socket) {
    //传入BluetoothSocket,实例化mmSocket
    mmSocket = socket;
    //输入/输出流 中间变量
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    // 输入输出流实例化
    try {
    tmpIn = socket.getInputStream();
    tmpOut = socket.getOutputStream();
    } catch (IOException e) {
    e.printStackTrace();
    }
    mmInStream = tmpIn;
    mmOutStream = tmpOut;
    }

    public void run() {
    byte[] buffer = new byte[1024];
    int bytes;
    // 连接成功时
    while (true) {
    try {
    // 在InputStream读数据
    bytes = mmInStream.read();
    //发送数据
    Events.bluetooth_Recycle bluetooth_recycle = new Events.bluetooth_Recycle();
    bluetooth_recycle.s = bytes;
    bluetooth_recycle.bytes = String.valueOf((char) bytes);
    EventBus.getDefault().post(bluetooth_recycle);
    } catch (IOException e) {
    break;
    }
    }
    }

    //写方法
    void write(byte[] bytes) {
    try {
    mmOutStream.write(bytes);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //关闭流连接
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException e) {
    }
    }
    }
  • 连接蓝牙

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //连接线程
    private ConnectThread mConnectThread;
    //管理连接进程
    private ConnectedThread mConnectedThread;

    //创建连接子线程
    mConnectThread = new ConnectThread(bluetoothDevice);
    //启动连接子线程
    mConnectThread.start();
  • 向蓝牙写入数据

    1
    2
    3
    4
    5
    6
    7
     //发送数据String,        主线程            优先级4       非粘性事件
    @Subscribe(threadMode = ThreadMode.MAIN, priority = 4, sticky = false)
    public void onEvent(Events.bluetooth_Send bluetoothSend) {
    String bytes = bluetoothSend.bytes;
    mConnectedThread.write(bytes.getBytes());
    LogUtil.d(Tag, "onEvent:Send" + bluetoothSend.bytes);
    }
  • 接收数据处理

    1
    2
    3
    4
    5
    6
      //数据转换                后台进程            优先级3       非粘性事件
    @Subscribe(threadMode = ThreadMode.BACKGROUND, priority = 3, sticky = false)
    public void onEvent(Events.bluetooth_Transformers bluetoothTransformers) {
    int bytes = bluetoothTransformers.bytes;
    //处理数据
    }


资料来源如下

  • 第一行代码(第二版)
  • Android Training Notifying the User
  • Android API 通知
  • 【Android】状态栏通知Notification、NotificationManager详解

编程环境

  • Android Studio 2.2.3

导语

  • OneTapDoze遇到的第一个难题,顺带记录Android 通知相关内容

最终效果

  • 绿色守护进入doze模式后,会在通知栏创建一个计时通知 如下图
    Screenshot_20170307-231118.png

  • 退出doze模式后,会指示进入doze和退出doze的时间段。
    Screenshot_20170307-231132.png

  • 我们要仿照的样式就是这样,进入退出doze,对应创建通知的代码都在doze模式改变的Broadcast中。

基础部分

创建通知

  • 行文前:
    Android每次版本几乎都会有通知API的改动,不同版本之间通知兼容性很是问题,为此我们使用support_v7 库中的NotificationCompat.Builder代替Notification.Builder二者使用方式相同

  • 涉及到的两个类

    • NotificationManager
    • Notification (support库中对应NotificationCompat)
  • NotificationManager

    • 状态栏通知的管理类,负责发通知、清除通知等
    • NotificationManager 是一个系统Service,必须通过 getSystemService()方法来获取
  • Notification

    • 具体的状态栏通知对象,可以设置icon、文字、提示声音、振动等参数
    • 一个Builder对象至少包含三个方面
      • 一个小图标,通过setSmallIcon()方法设置。
      • 通知标题,通过setContentTitle()方法设置。
      • 详细文本,通过setContentText()方法设置。
  • 简单示例

    • 创建通知构建器
      代码

      1
      2
      3
      4
      5
      6
      Notification notification = new NotificationCompat.Builder(this)
      .setContentTitle("这是通知标题")
      .setContentText("这是通知内容")
      //这里使用的是应用图标,一般没人这么干,就是为了方便
      .setSmallIcon(R.mipmap.ic_launcher)
      .build();
    • 发布通知

      • 获得NotificationManager的实例
      • 使用notify()方法发布通知。在调用notify()方法 指定通知的ID,(ID用于通知更新) 加载Notification实例
      1
      2
      3
      4
      int mNotificationId = 001;

      NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      manager.notify(mNotificationId, notification);
  • 效果如下

    • android 7.0 / 6.0 /4.0 通过
    • ScreenShot_20170202173430.png

更新通知

  • 可以创建一个全新的NotificationCompat对象,也可以在原NotificationCompat对象基础上修改,最后只要 .notify 方法中对应同一个 mNotificationId ,系统就会自动更新已有通知,代码不在累赘。

  • 基础部分到此,足矣

正题

  • 实现时间段的显示,肯定会保存系统进入doze 退出doze对应的时间点。再计算出中间经历的时间。

提取系统时间

  • java.util.Date
    这里用到了java中的 时间类型 java.util.Date
    java.util.Date 是java中常用时间类型,可以被SimpleDateFormat格式化format() 指定输出的时间格式
    比如我们需要 小时:分:秒
    SimpleDateFormat scanf = new SimpleDateFormat("HH:mm:ss");
    scanf.format(java.util.Date) 即可。

  • 提前当前系统时间

    1
    2
    java.util.Date time_after = null;
    time_after = new java.util.Date(System.currentTimeMillis());
  • 计算时间差
    理论上是两个时间相减再格式化输出即可,可惜没成,原因还没找到。于是土办法:

    • 调用 .getTime() 方法,将 java.util.Date 转化为毫秒计时(long)
    • 取两者差值
    • 转化为 时间长短 字符串返回,
  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     size = (time_after.getTime() - time_befor.getTime());
    string = scanf.format(time_befor) + "-" + scanf.format(time_after) + "="+ trform(size);

    String trform(long size) {
    long day, hour, min, secone;

    day = size / (1000 * 60 * 60 * 24); //以天数为单位取整
    hour = (size / (60 * 60 * 1000) - day * 24); //以小时为单位取整
    min = ((size / (60 * 1000)) - day * 24 * 60 - hour * 60); //以分钟为单位取整
    secone = (size / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60);

    if (day != 0) {
    return day + "d" + hour + "h" + min + "m" + secone + "s";
    } else if (hour != 0) {
    return hour + "h" + min + "m" + secone + "s";
    } else if (min != 0) {
    return min + "m" + secone + "s";
    } else {
    return secone + "s";
    }
    }

通知扩展布局

  • 在如下图片中,可以发现,通知在发出后,只默认显示最近一次的时间记录,其他时间记录可以在此条通知上下滑查看。明显与基础部分不同。
    Screenshot_20170307-231132.png

  • 此处应用了 Builder.setStyle() 拓展布局,顾名思义,这是用于拓展通知显示范围


使用扩展布局

  • 使用拓展布局
    • 构建一个 inboxStyle 对象
    • .addLine方法 添加 String inboxStyle 行
    • 使用 Builder.setStyle( inboxStyle )加载到通知
    • 等待通知发布即可。
  • 简单例程如下
    1
    2
    3
    4
    5
    6
    //创建 inboxStyle
    NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
    //添加 String 行 ,前提是 我已经创建了一个 String
    inboxStyle.addLine(string);
    // 在notification 设置 inboxStyle 在一堆 build 里面
    .setStyle(inboxStyle)
  • 清除原有内容,发布新的 inboxStyle 只需要 inboxStyle = new NotificationCompat.InboxStyle();

Doze模式改变广播

  • 对应 ACTION_DEVICE_IDLE_MODE_CHANGED

  • 坑:只能动态注册,静态注册无效,具体代码中powermanger.ACTION_DEVICE_IDLE_MODE_CHANGED 需要实例化 PowerManger ,而 PowerManger 又是与 Activity绑定,所以只有应用保持后台存活时才会进入广播。
  • 例程如下
    1
    2
    3
    4
    5
    6
    7
    8
    //实例化 PowerManager
    PowerManager powermanger = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
    //动态注册广播
    intentFilter = new IntentFilter();
    intentFilter.addAction(powermanger.ACTION_DEVICE_IDLE_MODE_CHANGED);
    //idlemodechage 是 BroadcastReceiver
    idlemodechage = new IdleModeChange();
    registerReceiver(idlemodechage, intentFilter);


资料来源如下

  • 第一行代码(第二版)
  • android6.0运行时权限详解

编程环境

  • Android Studio 2.2.3

导语

  • 内容提供器是安全的应用间共享数据途径,在正式进入之前需要了解一下 Android 自6.0以来权限的变化

Android 权限详解


资料来源:

  • android6.0运行时权限详解
  • Develop-API Guides-系统权限
  • 第一行代码(第二版)

Android权限介绍

  • Android 是一个权限分隔的操作系统
    每个应用系统标识(Linux 用户 ID 和组 ID)不同。系统各部分标识亦不相同。Linux 据此将不同的应用以及应用与系统分隔开来。
    更详细的安全功能通过“权限”提供,权限 会限制特定进程可以执行的具体操作,并且根据 URI 权限授权临时访问特定的数据段

  • 权限声明
    最常见的权限声明在 AndroidManifest.xml 文件中一个或多个uses-permission 标记,

    • 例如 声明短信权限
      1
      2
      3
      4
      5
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.android.app.myapp" >
      <uses-permission android:name="android.permission.RECEIVE_SMS" />
      ...
      </manifest>
  • android 6.0 权限机制
    android 6.0之前 权限及在安装的时候,根据权限声明产生一个权限列表,用户只有在同意之后才能完成app的安装。
    android 6.0 以后 引入了 运行时权限 功能,应用所需权限不再一次性在安装时申请,而是在运行时需要用到那种权限向用户申请,同时用户也可以随时取消该授权

  • 权限分类

    • Normal Permissions(普通权限)
      不涉及用户隐私,不需要用户进行授权的,例如 手机震动、访问网络等
    • Dangerous Permission(危险权限)
      涉及到用户隐私的,需要用户进行授权,例如 读取sdcard、访问通讯录等
      危险权限组如表1

应用运行时申请权限(适用于 Android6.0 及以上版本)

  • 通过一个简单的实例来说明过程

实例:

  • 拨打电话 CALL_PHONE _权限为例

    • 权限声明
      1
      <uses-permission android:name="android.permission.CALL_PHONE"/>
  • 拨打10010

    • 代码如下 构建一个隐式intent 启动拨打电话
      1
      2
      3
      Intent intent = new Intent(Intent.ACTION_CALL);
      intent.setData(Uri.parse("tel:10010"));
      startActivity(intent);
  • 在主界面上添加一个Button 在其onClick方法中拨打电话,运行程序!


  • 在An’d’roid 6.0 以下版本中 电话都可以正常拨出。Android 6.0 以上版本中 则可能会应用崩溃/无反应,查看logcat的打印日志可以看到 “Permission Denial”权限被禁止的信息,接下来尝试使用 运行时申请权限

  • 检查是否已获得授权
    int checkSelfPermission(Context context, String permission)方法

    • Context context context
      String permission 权限具体名称
      打电话对应 Manifest.permission.CALL_PHONE _
    • 方法返回 PERMISSION_GRANTED / PERMISSION_DENIED
    • 例程
      1
      2
      3
      4
      5
      6
      7
      8
      if(ContextCompat.checkSelfPermission
      (MainActivity.this,Manifest.permission.CALL_PHONE)
      !=PackageManager.PERMISSION_GRANTED)
      {
      ;
      }else {
      ;
      }
  • 申请权限
    void ActivityCompat.requestPermissions (Activity activity, String[] permissions, int requestCode)方法

    • Activity activity Activity实例
      String[] permissions 请求权限名
      int requestCode 请求码,大于0即可,对应在onRequestPermissionsResult 回掉方法中的requestCode

    • 例程

      1
      2
      ActivityCompat.requestPermissions(MainActivity.this,
      new String[]{Manifest.permission.CALL_PHONE},1 );
  • 授权后回掉
    无论用户是否授权都会进入回掉方法,需要重写该方法。
    void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)方法

    • int requestCode 在 requestPermissions 中传入的请求码
      String[] permissions 请求的权限,不为空
      int[] grantResults 授权结果 只有 PERMISSION_GRANTED / PERMISSION_DENIED 两个值

    • 例程

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Override
      public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
      switch (requestCode) {
      //请求码 1
      case 1:
      //授权结果(int) 的 长度是否大于0 与上 授权结果是否等于 PackageManager.PERMISSION_GRANTED
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      //授权通过执行
      } else {
      //授权没有通过
      }
      break;
      default:
      }
      }
  • 当用户拒绝后再次询问
    boolean ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission) (真心太长了)

    • Activity activity Activity实例
      String permission 权限名称
      应用第一次需要授权时,用户授权,该方法返回false。当用户拒绝授权,下次需要该权限时,该方法会返回true
    • 例程
      1
      2
      3
      4
      5
      6
      7
       if (ActivityCompat.shouldShowRequestPermissionRationale
      (PermissionActivity.this,Manifest.permission.READ_CONTACTS))
      {
      //第二次询问用户是否授权
      }else{
      //用户依旧拒绝后操作
      }
  • 例程源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class MainActivity extends AppCompatActivity {

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

    public void Button(View view){
    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:10010"));
    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:
    }
    }

    }
    1
    2
    3
    4
    5
    <Button
    android:onClick="Button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!" />


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

Android 中的主要数据存储

  • 有三种

  • 保存键值集
    要保存的相对较小键值集合,使用 SharedPreferences

  • 保存文件
    使用 Android 文件系统通过 File API 读取和写入文件,适合按开始到结束的顺序不跳过地读取或写入大量数据。 例如,图片文件或通过网络交换的任何内容

  • 在 SQL 数据库中保存数据
    Android内建的SQLite 数据库存储重复或结构化数据


  • PS:这里如果没有学过 SQL 数据库使用,推荐LitePal
  • Android开源数据库LitePal :郭霖(第一行代码作者)
  • 详细介绍在这里(懒人必备)Android开源数据库LitePal

保存键值集

  • 想要保存的相对较小键值集合

  • 调用SharedPreferences

  • 首先:获取SharedPreferences的句柄
    用户可以创建一个共享参数文件或访问一个已经存在的共享参数文件。
    具体途径有三条

    • getSharedPreferences (String name, int mode)
      • 第一个参数:SharedPreferences的名称,第二个:参数指定操作模式
      • 需要创建/读取 多个共享参数文件时使用,每个文件都拥有一个标识符,可以通过这个标识符通过该方法的第一个参数获得共享参数对象。可以通过APP中的任意Context对象调用它。
      • 示例
        1
        2
        //指定SharedPreferences名称为data  模式为MODE_PRIVATE:只有当前程序才有权限读取/修改
        SharedPreferences sharedPref = getSharedPreferences("data", MODE_PRIVATE);
    • getPreferences (int mode)
      • 参数:指定操作模式
      • 只创建 Activity 的一个SharedPreferences。在Activity 中使用。 方法会检索属于该 Activity 的默认共享首选项文件,无需提供名称
      • 示例
        1
        2
        //模式:MODE_PRIVATE:只有当前程序才有权限读取/修改
        SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
    • getDefaultSharedPreferences(context)
      • 参数:context
      • 自动以档期应用程序包名作为前缀命名SharedPreferences文件
  • 写入数据到共享参数中

    • SharedPreferences 调用 edit() 创建一个 SharedPreferences.Editor
      使用诸如 putInt() 和 putString() 方法写入的键和值
      最后调用commit() 以保存更改
      也可调用apply() 保存更改

  • commit() 与 apply() 相同/区别
  • 相同
    • 都是提交preference修改数据
  • 区别
    • apply没有返回值 commit返回boolean表明修改是否提交成功
    • apply是将操作提交到内存,而后异步真正提交到文件
      commit是同步的提交到文件
      多个commit同时提交,互相等待,效率较低
      apply直接覆盖,效率较高
    • apply没有任何错误提示,只是提交的动作

  • 示例
    1
    2
    3
    4
    5
    6
    //创建 .edit()
    SharedPreferences.Editor editor = sharedPref.edit();
    //写入字符串。key-值 TextView
    editor.putString("TextView","hello");
    //保存更改
    editor.commit();
  • 从共享参数中读取数据
    • 获取SharedPreferences的句柄
      方式同上
    • 调用比如getInt()或getString()方法,然后传入键值,如果键不存在,则会返回一个默认值
    • 示例
      1
      2
      3
      4
      5
      6
      //获取SharedPreferences的句柄
      SharedPreferences sharedPreferences = getSharedPreferences("data",MODE_PRIVATE);
      //读取键值对应数据
      String text = sharedPreferences.getString("TextView","");
      //打印到log中 验证
      Log.d("data",text);

保存到文件

  • Android 设备有两个文件存储区域: 内部存储 和 外部存储
    Android 设备早期内置的非易失性内存(内部存储),以及移动存储介质 微型 SD 卡等(外部存储)
    任何Android设备始终有两个存储空间,并且无论外部存储设备是否可移动,API 的行为均一致

  • Android的文件存储是以java流为基础,并优化了一些操作。java流不属于本文范围。

  • 涉及java流部分参考Android文件IO详解

内部存储

保存到内部存储

  • 某一应用对其内部存储 始终具有 进行读写的权限,而无需声明任何权限,在 Android N之前,内部文件可以通过放宽权限让其他应用访问。但是极不推荐以这种方式共享文件

  • 简单实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public void save(String inputText){
    //新建一个FileOutputStream对象(字节流)
    FileOutputStream out = null;
    //新建一个BufferedWriter (字符流)
    BufferedWriter writer = null;
    try{
    //out 实例化
    out = openFileOutput("data",Context.MODE_PRIVATE);
    //OutputStreamWrit 字节流转换为字符流
    //字符流绑定BufferedWriter(缓冲区)
    writer = new BufferedWriter(new OutputStreamWriter(out));
    //写入到文件
    writer.write(inputText);
    }catch (IOException e ){
    e.printStackTrace();
    } finally {
    try{
    //如果写入成功
    if (writer != null) {
    //关闭流
    writer.close();
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    }
    }
  • FileOutputStream:

    • 继承自 OutputStream 用于文件处理的节点流
    • 用于写入原始字节流到文件
  • FileOutputStream openFileOutput (String name, int mode)

    • 返回FileOutputStream对象
    • 第一个参数是文件名
    • 第二个参数是操作模式,有4种取值
      • MODE_PRIVATE(默认)_当同一文件名存在时覆盖原文件
      • MODE_APPEND _当同名文件存在时,在原文件末尾追加
      • 其余两种均在android4.2及以上版本中废弃
  • OutputStreamWrit

    • 继承自 Writer
    • 将字节流转换为字符流
  • BufferedWriter

    • 继承自Writer
    • 绑定字符输出流,提高具体的流对象的效率
    • .flush()方法 对缓冲区进行刷新,让数据到目的地
    • .close();方法 关闭缓冲区,即关闭绑定的流
从内部存储读取文件
  • 简单实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public String load(){
    //新建FileInputStream对象
    FileInputStream in = null;
    //新建 BufferedReader对象
    BufferedReader reader = null;
    //新建一个StringBuilder 空
    content = new StringBuilder();
    try{
    //FileInputStream对象实例化
    in = openFileInput("data");
    //字节流转换为字符流
    //缓冲流绑定字符流
    reader = new BufferedReader(new InputStreamReader(in));
    //空 String
    String line = "";
    //读取文件内容,并判断是否读取完成
    while((line = reader.readLine())!=null){
    //读取内容添加到 StringBuilder 对象中
    content.append(line);
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    finally {
    if(reader != null){
    try{
    //关闭流
    reader.close();
    }catch (IOException e){
    e.printStackTrace();
    }
    }
    }
    //将读取结果 由StringBuilder转换为String 并返回
    return content.toString();
    }
  • FileInputStream

    • 继承自 InputStream,用于文件处理的节点流
    • 从文件中读取
  • FileInputStream openFileInput(String filename)

    • 返回FileInputStream对象
    • 参数为 读取的文件名
  • InputStreamRead

    • 将字节流转换为字符流
  • BufferedReader

    • 缓冲流

外部存储

  • 占坑


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

BroadcastReceiver基础

  • 首先祭出官方文档及中文翻译

https://developer.android.com/reference/android/content/BroadcastReceiver.html
http://www.jianshu.com/p/1b56172b0c77

BroadcastReceiver概述

  1. @别路寻忆
    Android中的四大组件是 Activity、Service、Broadcast和Content Provider。而Intent是一个对动作和行为的抽象描述,负责组件之间程序之间进行消息传递。那么Broadcast Receiver组件就提供了一种把Intent作为一个消息广播出去,由所有对其感兴趣的程序对其作出反应的机制。
  2. @zuolongsnail专栏
    • 广播接收器是一个专注于接收广播通知信息,并做出对应处理的组件。很多广播是源自于系统代码的──比如,通知时区改变、电池电量低、拍摄了一张照片或者用户改变了语言选项。应用程序也可以进行广播──比如说,通知其它应用程序一些数据下载完成并处于可用状态。

    • 应用程序可以拥有任意数量的广播接收器以对所有它感兴趣的通知信息予以响应。所有的接收器均继承自BroadcastReceiver基类。

    • 广播接收器没有用户界面。然而,它们可以启动一个activity来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

广播分类

  • 触发广播发生的事件分类有两种

    1. Android系统广播事件
      由android设备状态变化而触发的系统广播如:
      Intent.ACTION_POWER_CONNECTED;
      //插上外部电源时发出的广播
      Intent.ACTION_SCREEN_ON;
      //屏幕被打开之后的广播
      ACTION_TIME_CHANGED
      //系统时间改变而触发的广播
    2. 自定义的广播事件
      这个比较好理解了,比如qq当我们再另外一台手机上登陆时,手头的这个手机qq就会自动下线。腾讯的服务器通过后台服务启用了自定义广播来终结正在运行的Activity,详情之后会有例程,不再累赘。
  • 能够被接收的广播类型

    1. 普通广播(Normal broadcasts):
      (由 Context.sendBroadcast发出)异步发出。所有广播接收器都可以在同一时间接收广播。广播接收者无法接收广播的处理结果或者阻断广播的传递。

    2. 有序广播(Ordered broadcasts):
      (由 Context.sendOrderedBroadcast发出)每次只发送给一个广播接收器。当每个广播接收器依次执行时,它可以向下一个广播接收器传播结果,或者阻断该广播,使得该广播不能被下一个广播接收器接收到。

    • 通俗解释:
      普通广播就相当于小区/村委会的大喇叭,有事发生(触发广播)通知所有的人(广播接收器),全功率的大喊大叫确保所有人(广播接收器)都能听到。而有人(广播接收器)在睡觉被吵醒但是又没办法砸了那个大喇叭,只能继续听着(无法接收广播的处理结果或者阻断广播的传递)。
      有序广播 就相当于间谍机关的绝密消息传递。绝密到手(触发广播)间谍秘密汇总给上线A(广播接收器A)。本来 上线(广播接收器A)应该 听取完毕重新整理情向首长B(广播接收器B)报告(向广播接收器B传播 广播接收器A修改的结果),但是A被收买,将情报隐匿了,没有向首长B汇报(广播接收器A阻断该广播,使得该广播不能被下一个广播接收器B接收到)。(PS结局首长B错误带人炸了村委会,该睡觉的人终于可以安生睡觉了!)
  • 广播事件注册有两种

    1. 静态注册,就是在AndroidManifest.xml文件中定义,注册的广播接收器必须要继承BroadcastReceiver.
    2. 动态注册,是在程序中使用Context.registerReceiver注册,注册的广播接收器相当于一个匿名类。两种方式都需要IntentFIlter。
    • 例程见下节。

动态注册(在代码中注册)

  • 在代码中通过registerReceiver()注册。app关闭后,该接收器也会随之销毁。

  • 首先定义一个内部子类NetworkChangeReceiver继承自 BroadcastReceiver

    1
    2
    3
    4
    5
    6
    7
    8
    class NetworkChangeReceiver extends BroadcastReceiver {
    @Override

    //重写onReceive,接收到广播后提示消息
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "network is available网络已变化", Toast.LENGTH_SHORT).show();
    }
    }
  • 在onCreate()方法中创建IntentFilter实例和NetworkChangeReceiver实例。并在IntentFilter实例中添加网络变化时系统广播对应值。随后传入registerReceiver()中注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //创建IntentFilter实例
    intentFilter = new IntentFilter();
    //添加对应系统广播
    intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
    //添加NetworkChangeReceiver实例
    networkChangeReceiver = new NetworkChangeReceiver();
    //动态注册
    registerReceiver(networkChangeReceiver, intentFilter);
    }
  • 动态注册的广播接收器最后要取消注册。在onDestroy()方法中调用unregisterReceiver()销毁动态注册的广播接收器。

    1
    2
    3
    4
     protected void onDestroy(){
    super.onDestroy();
    unregisterReceiver(networkChangeReceiver);
    }
  • 最后要在AndroidMainfest.xml中声明查询系统网络状态的权限。

    1
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  • 运行效果如如所示(模拟器)

静态注册(xml中注册)

  • 直接在Manifest.xml文件中配置广播接收者
  • 例程为了方便同样以android.net.conn.CONNECTIVITY_CHANGE为例,与动态注册相同。

不使用内部子类(第一行代码)

  • 新建名称为BootCompleteReceiver的java class 代码如下

    1
    2
    3
    4
    5
    6
       public  class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "网络改变", Toast.LENGTH_SHORT).show();
    }
    }
  • 在AndroidManifest.xml文件中注册广播接收器
    代码如下

    1
    2
    3
    4
    5
    <receiver android:name=".BootCompleteReceiver">
    <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
    </intent-filter>
    </receiver>
  • 在AndroidManifest.xml中声明权限

    1
    2
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>

使用内部子类(在Activity中定义)


在这晕圈了一下午,找不出毛病。看书多仔细吧,书上说了不用内部子类。
详细资料在这里

http://blog.csdn.net/chdjj/article/details/19496567


  • 清单文件注册广播接收者时,广播接收者的名字格式需要注意因为是内部类,所以需要在内部类所在的类与内部类之间加上$符号(这一点在AndroidStudio中输入时有提示)
  • 内部类在声明时一定要写成静态内部类(class关键字前加上static)。否则会抛出异常(广播发生时,应用停止运行)

  • 在MainActivity中新建子类

    1
    2
    3
    4
    5
    6
     public static class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "Boot complete", Toast.LENGTH_SHORT).show();
    }
    }
  • 在AndroidManifest.xml中注册广播接收器

    1
    2
    3
    4
    5
    <receiver android:name=".MainActivity$BootCompleteReceiver">
    <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
    </intent-filter>
    </receiver>
  • 在AndroidManifest.xml中声明权限

    1
    2
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
  • 运行效果同动态注册

使用广播接收器注意事项

  • 在onReceive()方法中不宜添加过多逻辑/耗时操作,广播接收器没有多线程,一旦时间过长,程序就会报错。
  • 广播接收器一般为启动其他组件作用。

发送标准广播


  • 发送标准广播之前,首先要注册一个作为目标的广播接收器。(过程略,只上代码)
    新建MyBroadcastReceiver.class
    1
    2
    3
    4
    5
    6
    7
       public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent)
    {
    Toast.makeText(context,"MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
    }
    }
    在Xml中注册(静态)
    1
    2
    3
    4
    5
    <receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>

  • xml中注册一个Button

    1
    2
    3
    4
    5
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="SendBroad"
    android:text="SendBroad" />
  • MainActivity中新建 SendBroad()函数
    首先构建一个 Intent对象,将自定义的广播值填入。再调用sendBroadcast方法将广播发送出去。

    1
    2
    3
    4
    5
        public void SendBroad(View view)
    {
    Intent intent = new Intent("com.example.broadcasttest.MyBroad");
    sendBroadcast(intent);
    }
  • 发送的是标准广播。运行效果如下图。
    Screenshot_20160910-220121.png

发送有序广播


  • 首先新建一个Broad2 的工程。同样接收Broad发送的广播。
    代码如下
    1
    2
    3
    4
    5
    6
    7
       public class AnotherBroadcast extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent)
    {
    Toast.makeText(context," AnotherBroadcast",Toast.LENGTH_SHORT).show();
    }
    }
    1
    2
    3
    4
    5
    <receiver android:name=".AnotherBroadcast">
    <intent-filter android:priority="10">
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>
  • 测试效果:当摁下发送广播按钮后,弹出两个提示。

  • 修改Broad项目中onClick对应事件。将sendBroadcast()改为sendOrderedBroadcast();发送有序广播。

    1
    2
    3
    4
    5
        public void SendBroad(View view)
    {
    Intent intent = new Intent("com.example.broadcasttest.MyBroad");
    sendOrderedBroadcast(intent,null);
    }
  • 效果与发送标准广播相同(还未定义优先级/截断等)

  • 定义优先级,再Broad的AndroidMainfest.xml中修改注册的广播添加android:priority="100"优先级100

    1
    2
    3
    4
    5
    <receiver android:name=".MyBroadcastReceiver">
    <intent-filter android:priority="100">
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>
  • 在Broad2的AndroidMainfest.xml中添加android:priority="10"优先级10

    1
    2
    3
    4
    5
    <receiver android:name=".AnotherBroadcast">
    <intent-filter android:priority="10">
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>
  • 发送广播后MyBroadcastReceiver最先收到广播。

  • 截断广播。有序广播中前一个广播接收器可以截断广播传播。添加 abortBroadcast();即可。

    1
    2
    3
    4
    5
    6
    7
    8
       public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent)
    {
    Toast.makeText(context,"MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
    abortBroadcast();
    }
    }
  • 再次点击按钮发送广播,只有MyBroadcastReceiver可以接收到发送的广播。

本地广播

  • 只在app应用内部传递的广播。注册过程类似于动态注册。

  • 定义一个内部类LocalReceiver继承自BroadcastReceiver

    1
    2
    3
    4
    5
    6
        public static class LocalReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
    }
    }
  • 首先在onCreate()方法中通过localBroadcastManager.getInstan得到一个LocalBroadcastManager的实例,再创建IntentFilter实例和LocalReceiver实例。并在IntentFilter实例中添加广播。随后传入localBroadcastManager.registerReceiver()中注册本地广播。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    private IntentFilter intentFilter;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);

    localBroadcastManager = localBroadcastManager.getInstance(this);

    intentFilter = new IntentFilter();
    intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
    localReceiver = new LocalReceiver();
    localBroadcastManager.registerReceiver(localReceiver, intentFilter);
    }
  • 在Activity的onDestroy()中销毁注册。

    1
    2
    3
    4
    5
    @Override
    protected void onDestroy(){
    super.onDestroy();
    localBroadcastManager.unregisterReceiver(localReceiver);
    }
  • 在Button对应的函数中调用localBroadcastManager.sendBroadcast发送本地广播。

    1
    2
    3
    4
    5
        public void SendBroad(View view)
    {
    Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
    localBroadcastManager.sendBroadcast(intent);
    }
  • 效果基本同上,不加累赘。不过在Broad2中是怎样都搜不到广播了。

  • 本地广播特点

    • 明确广播只在应用内部,传递数据无需担心泄密。
    • 其他程序广播无法发送至程序内部。
    • 本地广播比全局广播更为高效。


资料来源如下

  • 第一行代码(第二版)
  • Android官方文档之App Components(Fragments)

编程环境

  • Android Studio 2.2.3

导语

  • Fragments的艺术之旅

Fragments简介

  • Fragment—片段 是Android 在 Android 3.0(API 11 级)中引入了,主要是为了给大屏幕(如平板电脑)上更加动态和灵活的 UI 设计提供支持。
  • 可以将多个片段组合在一个 Activity 中来构建多窗格 UI,以及在多个 Activity 中重复使用某个片段。可以将片段视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,可以在 Activity 运行时添加或删除片段(类似于不同 Activity 中重复使用的“子 Activity”)
  • Google官方文档(中文)

创建Fragments

  • fragment创建过程与Activity类似。

  • 创建Fragment,需要继承一个Fragment类,并实现Fragment的生命周期回调方法,如onCreate(), onStart(), onPause(), onStop()等

  • 一般来说,在Fragment中应至少重写以下这些生命周期方法
    必须重写的时onCreateView()方法.

    • onCreate():创建Fragment实例时,系统回调的方法。在该方法中,对一些必要的组件进行初始化
    • onCreateView():Fragment上绘制UI时,回掉该方法。返回一个View对象,表示Fragment的根视图;若Fragment不需要绑定示图,可以返回null
    • onPause():当用户离开Fragment时回调。在该方法中,对Fragment的数据信息做持久化的保存工作

创建一个Fragment类

  • 新建first_fragme 继承自Fragment

  • Android Studio中 对应Fragment 包有两个,选择support-v4 _(这个版本可以再Android版本中保持Fragment特性一致)

  • Fragment并非一定要绑定一个布局文件,下面会提到。

    1
    2
    3
    4
    5
    6
    7
    public class FirstFragment extends Fragment {
    @Override
    public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle saveInstanceState){
    View view = inflater.inflate(R.layout.fragment_firest,container,false);
    return view;
    }
    }
  • 与此对应的fragment_firest布局文件 _
    LinearLayout加一个Button,背景颜色设置为了靛蓝色。

  • onCreateView()方法

    • ViewGroup来自宿主Activity容器布局,Fragment的布局将其作为根视图插入至该视图中。
    • Bundle用于回传之前占据该位置的Fragment实例所保存的Bundle信息,当该Fragment的新实例处于resume状态时,该参数被回传
  • inflate() 方法

    • 参数1(int):需要绑定的Layout的资源ID;

    • 参数2(ViewGroup):绑定的Layout布局的父视图;

    • 参数3(boolean):是否需要将参数1的Layout资源依附于,参数2的ViewGroup上,false,表示不依附。(系统已经默认将Layout插入至ViewGroup中,若为true,将添加一层冗余的视图

在XML将fragment添加到activity

  • 在activity_main.xml中添加如下代码。跟添加一个layout没有太大区别。

    1
    2
    3
    4
    5
    6
    <fragment
    android:id="@+id/left_fragment"
    android:name="ljy.com.fragmenttest.fragment_firest"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"/>
  • 至此一个fragment添加完毕。为了对比,再新建一个second_fragment,背景颜色不同,两者同时添加进activity_main中。

  • Screenshot_1476109113.png

  • 一般来说必须为fragment设定唯一的身份标识,以便当宿主Activity为restart状态时可以恢复fragment

    • android:id属性为fragment指定唯一ID
    • android:tag属性为fragment指定唯一字符串标识
    • 未指定,则该fragment的标识为其父容器控件的ID

动态添加碎片


  • 处理片段时,请谨记:Activity 布局必须包含一个可以插入片段的容器 View

  • 添加或移除片段必须使用 FragmentManager 创建 FragmentTransaction, FragmentTransaction将提供添加、移除、替换片段以及执行其他片段事务所需的 API。Activity 内调用 getSupportFragmentManager() 以获取 FragmentManager

    1
    2
    rightfragment right = new rightfragment();
    FragmentManager fragmentManager = getSupportFragmentManager();
  • 调用 beginTransaction() 创建一个 FragmentTransaction事务,并调用 add() 添加一个片段,做好更改准备时,调用 commit()

    1
    2
    3
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    transaction.replace(R.id.right_layout,right);
    transaction.commit();
  • 示例:

    • 首先改造一下activity_main.xml_
      FrameLayout包含一个left_fragment

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
        <FrameLayout
      android:id="@+id/right_layout"
      android:layout_width="0dp"
      android:layout_height="match_parent"
      android:layout_weight="1">

      <fragment
      android:id="@+id/left_fragment"
      android:name="com.example.fragment_text.leftfragment"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      />
      </FrameLayout>
    • 在MainActivity的onCreate方法中添加fragment

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      //新建准备替换的Fragment实例
      rightfragment right = new rightfragment();

      //新建FragmentManager
      FragmentManager fragmentManager = getSupportFragmentManager();
      // 调用beginTransaction() 创建FragmentTransaction
      FragmentTransaction transaction = fragmentManager.beginTransaction();

      //FragmentTransaction内处理添加/替换等。
      transaction.replace(R.id.right_layout,right);

      //最后执行commit()方法
      transaction.commit();

管理Fragments/执行Fragment事务

  • Activity中管理Fragment,使用FragmentManager(

    1
    FragmentManager fragmentManager = getFragmentManager();

    可以实现的操作

    • findFragmentById()方法获取由Activity管辖的绑定了UI的Fragment实例
    • popBackStack()方法将Fragment从后退栈中弹出
    • addOnBackStackChangedListener()方法注册监听器,用于监听后退栈的变化
  • Fragment可实现动态添加、删除、替换 等 操作,每一组向Activity提交的变化称为事务,使用FragmentTransaction这操作事务,调用beginTransaction()开启一个事务

    1
    2
    FragmentManager fragmentManager = getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    可实现对Feagment的操作

    • add() //添加
    • remove() //移除
    • replace() //替换
  • 事务要在Activity中生效,调用commit()方法提交

  • commit()方法之前,调用addToBackStack()方法,可以将该事物添加到由Activity管辖的Fragment返回栈中。点击Back键即撤销该事物提交的更改


  • 使用FragmentTransaction操作事务时注意

    • commit()必须在最后调用
    • 一个布局容器中添加多个Fragment,加入的顺序决定了这些Fragment绑定的UI视图在View树中的层级顺序
  • commit()方法提交后,并不会立即执行事务,UI更新只能在主线程中进行,主线程空闲时,才会执行事务操作
    Android也提供了 在UI线程中调用executePendingTransactions()方法,使commit()方法调用后立即执行提交的事务(一般用不到)


与 Activity 通信

  • Fragment 中获取 Activity 实例
    getActivity()方法可以获取到 Activity 实例
    1
    MainActivity activity = (MainActivity) getActivity();
    调用Activity中的试图也很简单,使用findViewById()即可,getActivity()方法返回的既是一个Context对象
    1
    View listView = getActivity().findViewById(R.id.list);

  • 在Fragment中使用Context对象,getActivity()方法,只能是在fragment已经依附于Activity后才能调用。当fragment未依附于某个Activity、或fragment已经处于其生命周期的末尾而不再依附于某个Activity时,调用getActivity()方法会直接返回null

  • Activity 中获取 Fragment 实例()
    FragmentManger提供了findFragmentById()方法

    1
    RightFragment fragment = (RightFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

    如果使用的使support-V4包,则 getFragmentManager()改为getSupportFragmentManager(),代码如下:

    1
    RightFragment fragment = (RightFragment)getSupportFragmentManager().findFragmentById(R.id.example_fragment);
  • Fragment与Frangment之间也可以通过宿主Activity,获取到另一个Fragment实例,调用另一个Fragment中的方法

为Activity创建事件回调

  • 占坑,

Frangment的生命周期

  • 与Activity极为相似,且 Frangment的生命周期与宿主Activity有很大关联

  • 有3种状态

    • Resumed继续:宿主Activity处于running,Fragment处于可见状态
    • Paused暂停:另一个Activity处于前台并获得了焦点,该Fragment的宿主Activity并未被全部遮挡
    • Stopped停止:Fragment不可见或Fragment已被Activity移除,宿主Activity被回收时,Fragment也将被回收
  • fragment_lifecycle.png

宿主Activity的影响

  • activity_fragment_lifecycle.png

重要回调方法

  • onAttach():fragment关联Activity时回调
  • onCreateView():fragment绑定UI视图(加载布局)时回调
  • onActivityCreated():宿主Activity创建完毕 (宿主Activity的onCreate()方法返回) 后调用
  • onDestroyView():与fragment绑定的UI视图被移除时回调
  • onDetach():fragment不再依附于Activity时回调

  • Activity 与Fragment在生命周期之间的最显著差异在于它们在其各自返回栈中的存储方式。
    默认情况下,Activity 停止时会被放入由系统管理的 Activity 返回栈(以便用户通过返回按钮回退到 Activity),
    Frament仅当您在移除片段的事务执行期间通过调用 addToBackStack() 请求保存实例时,系统才会将Fragment放入由宿主 Activity 管理的返回栈。

  • 一旦Activity处于resume状态时,可以自由地添加或移除fragment,也就是说,只有当Activity的状态为resume时,fragment才能够自由地控制自己的生命周期


资料来源如下

  • 第一行代码(第二版)
  • RecyclerView使用详解—六和敬
  • RecyclerView使用介绍— Jin Yudong
  • Android RecyclerView 使用完全解析 体验艺术般的控件—鸿洋_
  • 创建列表与卡片—Android Developer

编程环境

  • Android Studio 2.2.3

导语

  • RecyclerView内容较ListView更多,初期只能更新一些基础内容,高级的用法随时更新,长期跟进

简介

  • RecyclerView是用于取代ListView的组件,第一次出现是在2014年google I/O大会,内置在是Android L及以上版本的SDK中。
  • 对比与ListView,RecyclerView弥补了ListView中的效率问题,同时支持更多的显示效果,代码逻辑更为清晰

基本使用步骤


  • RecyclerView定义在support库中,使用RecyclerView之前必须在添加依赖
  • build.gradle中添加
    compile 'com.android.support:recyclerview-v7:25.1.0'

  • RecyclerView项目结构如下:
    RecyclerView.png

  • 要使用RecyclerView,需要指定一个Adapter适配器和一个LayoutManager布局管理器

  • Adapter适配器:作用与ListView中使用的Adapter相同,都是将数据与对应item的界面进行绑定
    所不同的是:RecyclerView中适配器必须继承自RecyclerView.Adapter,且 强制使用了ViewHolder

  • LayoutManager布局管理器:每一个item如何进行排列,何时展示和隐藏。
    重用View时,LayoutManager会向Adapter适配器请求新的数据替换旧的数据,避免了View的冗余和频繁调用findViewById

    LayoutManager的引入 使得各种布局排列编写,变的格外容易,这也是RecyclerView优于ListView的一个地方

  • 目前RecyclerView 内置3种LayoutManager:

    • LinearLayoutManager 横向/竖向布局
    • GridLayoutManager 网格布局
    • StaggeredGridLayoutManager 瀑布流布局
  • MainActivity中 RecyclerView 设置

    1. 创建RecyclerView对象
      RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycle_view);

    2. 设置LayoutManager显示规则

    3. 设置适配器
      recyclerview.setAdapter(adapter);

简单实例

由一个简单的实例+详细分析

  • 添加依赖
    打开app/build.gradle文件,在dependencies闭包下添加依赖库
    版本与你工程的com.android.support:appcompat-v7:25.1.0版本对应

    1
    compile 'com.android.support:recyclerview-v7:25.1.0'

    之后AndroidStudio会开始同步

  • 添加RecyclerView到xml文件,基本与ListView一致,不过RecyclerView并非内置在SDK中,这里需要写出完整的包路径

    1
    2
    3
    4
    5
    <android.support.v7.widget.RecyclerView
    android:id="@+id/recycle_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />
  • 要展示的依旧是水果+图片形式,
    图片资源在第一行代码第二版源码 /chapter3/ListViewTest\app\src\main\res\drawable-hdpi下,同时将Fruit类和fruit_item.xml一并复制,这里给出两者源码,不再加分析_

    • Fruit类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      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_item.xml _
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="horizontal"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

      <ImageView
      android:id="@+id/fruit_image"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" />

      <TextView
      android:id="@+id/fruit_name"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:layout_marginLeft="10dp" />

      </LinearLayout>
  • 准备RecyclerView适配器,适配器需要继承自 RecyclerView.Adapter,将泛型指定为 .ViewHolder,(ViewHolder为在适配器的一个内部类),并重写3个方法

    1
    2
    3
    public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType)//创建返回ViewHolder实例
    public void onBindViewHolder(ViewHolder holder,int pisition)//数据与界面绑定
    public int getItemCount() // 返回数据的数量

    新建 FruitAdapter 继承自 RecyclerView.Adapter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
      //继承自RecyclerView.Adapter,泛型为  FruitAdapter .ViewHolder
    public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    //私有Fruit列表
    private List<Fruit> mFruitList;

    //新建内部类ViewHolder继承自RecyclerView.ViewHolde有每个Item的的所有界面元素
    static class ViewHolder extends RecyclerView.ViewHolder{
    ImageView fruitImage;
    TextView fruitName;
    //ViewHolder构造函数,传入View,通常为RecyclerView子项的外层布局(本例为fruit_item.xml)
    public ViewHolder(View view){
    super(view);
    //findViewById获取 Image/Name实例
    fruitImage = (ImageView)view.findViewById(R.id.fruit_image);
    fruitName = (TextView)view.findViewById(R.id.fruit_name);
    }
    }

    //FruitAdapter构造函数,将数据源传入全局变量mFruitList
    public FruitAdapter(List<Fruit>fruitList){
    mFruitList = fruitList;
    }

    @Override
    //创建ViewHolder实例
    public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType){
    //加载布局文件
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
    //调用ViewHolder构造函数
    ViewHolder holder = new ViewHolder(view);
    //返回ViewHolder实例
    return holder;
    }

    @Override
    //对 RecyclerView子项进行赋值,有新的子项进入屏幕显示范围时调用
    public void onBindViewHolder(ViewHolder holder,int pisition){
    Fruit fruit = mFruitList.get(pisition);
    holder.fruitImage.setImageResource(fruit.getImageId());
    holder.fruitName.setText(fruit.getName());
    }

    @Override
    //返回 RecyclerView子项数量
    public int getItemCount(){
    return mFruitList.size();
    }

    }
  • MainActivity中使用RecyclerView

    • 创建RecyclerView对象

    • 设置LayoutManager显示规则(默认竖向滚动)

    • 设置适配器

    代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
     public class MainActivity extends AppCompatActivity {
    //私有列表
    private List<Fruit> fruitList = new ArrayList<Fruit>();

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

    //初始化Fruits数据
    initFruits();
    //获取RecyclerView实例
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycle_view);

    //创建LinearLayoutManager,并设置入recyclerView
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    recyclerView.setLayoutManager(layoutManager);

    //新建适配器,并传入
    FruitAdapter adapter = new FruitAdapter(fruitList);
    recyclerView.setAdapter(adapter);
    }

    //初始化Fruits类
    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);
    }
    }
    }
  • 这样最简单的 RecyclerView 就搭建完了效果如下
    ScreenShot_20161216105246.png

  • 代码比ListView稍多,但是逻辑比较清晰。

横向/瀑布流/网格布局

  • 只需要设置相应LayoutManager即可 /手动滑稽

横向

  • 在 LinearLayoutManager 中修改几行代码即可

  • 修改 LinearLayoutManager 代码,添加一行,搞定!

    1
    layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
  • 当然需要简单修改一下item的布局文件,LinearLayout改为垂直排列,宽度固定100dp,图片/文字居中
    效果如下
    ScreenShot_20161216105723.png

瀑布流

  • 这需要 StaggeredGridLayoutManager 布局管理器,可以 竖向/横向 滚动

  • 代码如下

    1
    2
    //瀑布流布局,3行,竖向
    StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
  • 调整布局文件,效果如下
    ScreenShot_20161216215538.png

网格

  • 这需要 GridLayoutManager 布局管理器
  • 代码如下
    1
    2
    //网格布局,两行
    GridLayoutManager layoutManager = new GridLayoutManager(this,3);
  • 调整布局文件后,效果如下
    ScreenShot_20161216220321.png

注册点击事件

  • RecyclerView 中并没有提供注册监听器的方法,需要子项View自行注册
  • 部分代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
      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);
    }
    }

    @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;
    }


  • 资料来源如下
  • 第一行代码(第二版)
  • ListView
  • Android ListView工作原理完全解析,带你从源码的角度彻底理解

  • ListView是所有原生控件中使用频率最高和最复杂的,涉及知识点也较多,专门抽出一篇来记录,涉及一些过程分析,希望自己能写完

ListView的简单使用

  • 先来看一下一个简单的实例
    代码如下

    • 在activity_main.xml中添加_ListView
      1
      2
      3
      4
      5
      <ListView
      android:id="@+id/list_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      </ListView>
    • 修改MainActivity的代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
        //要显示的数组,屏幕长了点,为了显示滑动效果多增加了几项。
      private String[] data = {"apple","banana","orange","watermelon","pear","grape","pineapple","strawberry","cherry","mango","A","B","C","D","E","F"};

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
      setSupportActionBar(toolbar);

      //为适配器绑定数据
      ArrayAdapter<String> adapter = new ArrayAdapter<String>(MainActivity.this,android.R.layout.simple_list_item_single_choice,data);
      //得到ListView对象的引用
      ListView listView = (ListView) findViewById(R.id.list_view);
      //适配器内容传递
      listView.setAdapter(adapter);
      });
      }
  • 效果如图
    Screenshot_20160812-170816.png

  • 步骤其实比较简单

    • 准备要显示的数据 ——String[] data数组
    • 构建适配器 ——ArrayAdapteradapter
    • 将适配器添加到ListView ——listView.setAdapter(adapter);
  • 详解

    • ListView首先是用来展现大量数据的,
      数据源的来源可以是网络下载,程序内置、数据库提取等,本例中则是简单的定义了一个String类型 data数组。
      数据源的类型/种类繁多,使得ListView无法直接适配数据源,直接适配数据源将导致代码的臃肿和效率的低下。ListView与数据源之间需要一个过度
    • 适配器:Adapter,Adapter在ListView和数据源之间起到了一个桥梁的作用。正是Adapter的使用ListView的使用变得要比其它控件复杂得多。

Adapter适配器

  • Adapter适配器在ListView和数据源之间起到了一个桥梁的作用Adapter的接口都是统一的,因此ListView不用担心任何适配方面的问题。

    Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配

    这样就解决了数据源适配的难题,并且还拥有相当不错的扩展性

  • 简易的示图(来自 郭霖的博客)
    SouthEast.png

  • 常用Adapter

    ArrayAdapter——用来绑定一个数组,支持泛型操作

    SimpleAdapter——用来绑定在xml中定义的控件对应的数据

    SimpleCursorAdapter——用来绑定游标得到的数据

    BaseAdapter——通用的基础适配器

  • 实例中使用的是ArrayAdapter,ArrayAdapter具有多个构造函数重载,这里使用的是字符串类型,

  • ArrayAdapter的构造函数

    • 当前上下文
    • ListView子项的id,本例中使用的是android.R.layout.simple_list_item 这是Android内置的一个布局文件
    • 需要适配的数据
      之后适配器就构建完成,最后传递进LIstView中即可

自定义适配器

  • 从第一行代码第二版的源码LiseViewTest目录下拷贝drawble_hdpi _到你的工程下

  • 新建Fruit类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Fruit {

    private String name;
    private int imageId;
    //构造函数 水果名称/水果对应图片资源ID
    public Fruit(String name, int imageId) {
    this.name = name;
    this.imageId = imageId;
    }

    public String getName() {
    return name;
    }

    public int getImageId() {
    return imageId;
    }
    }
  • 在layout目录下新建fruit_item.xml _

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
    android:id="@+id/fruit_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

    <TextView
    android:id="@+id/fruit_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:layout_marginLeft="10dp" />

    </LinearLayout>

    一个ImageView和TextView ,水平居中显示

  • 自定义适配器继承自ArrayAdapter 泛型指定为fruit类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     //继承自ArrayAdapter
    public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceId;
    //构造函数 /上下文 ListView子项布局id 数据
    public FruitAdapter(Context context, int textViewResourceId,
    List<Fruit> 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;
    }
    }
    • getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView
    • LayoutInflater.inflate()方法来去加载布局。接收3个参数,第三个参数为false表示只在父布局声明的layout属性有效,但不会为这个view添加父布局,最后设定为接下来会对这个view进行一些属性和值的设定,最后将view返回。
  • 修改MainActivity的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class MainActivity extends AppCompatActivity {

    //新建Fruit类的数组
    private List<Fruit> fruitList = new ArrayList<Fruit>();

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

    // 初始化水果数据
    initFruits();
    //设置FruitAdapter适配器
    FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);
    //得到ListView的引用
    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);
    }
    }
    }
  • 只需要修改fruit_item.xml,即可定制界面_

提升ListView的效率

convertView参数

  • 针对 Fruit fruit = getItem(position); // 获取当前项的Fruit实例
    每次滑动ListView时,getView()方法都会将布局都会重新加载一边
  • convertView参数:将之前加载完成的布局进行缓存。
  • 借助convertView参数,在每次加载View之前查询convertView是否为空,为空则重新加载,不为空则从convertView中取
  • 代码部分如下
    1
    2
    3
    4
    5
    6
    7
    8
    View view;
    Fruit fruit = getItem(position); // 获取当前项的Fruit实例

    if (convertView == null) {
    view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
    }else {
    view = convertView;
    }
  • ListView不会再重复加载布局了

ViewHolder

  • ViewHolder不是Android的开发API,而是一种设计方法

  • 针对
    ImageView fruitImage = (ImageView)view.findViewById(R.id.fruit_image); TextView fruitName = (TextView)view.findViewById(R.id.fruit_name);
    每次布局实例化后,findViewById都会重新执行获取控件实例

  • 新增内部类 ViewHolder,利用Tag附加到对应View中,即每次加载布局时,对应的 获取控件实例的操作也一并执行,并储存在View中。这样 获取控件实例的操作直接缓存在View中,不会再重复执行

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
     @Override
    public View getView(int position, View convertView, ViewGroup parent) {
    Fruit fruit = getItem(position); // 获取当前项的Fruit实例
    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;
    }
    //ViewHolder 内部类
    class ViewHolder {

    ImageView fruitImage;

    TextView fruitName;

    }

ListView中的点击事件

  • 比较简单,再MainActivty的onCreate方法中添加
  • 代码如下
    1
    2
    3
    4
    5
    6
    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();
  • setOnItemClickListener为ListView注册了一个监听器,点击发生时回掉 onItemClick()方法,该方法通过position参数判断具体子项。该处是执行Toast