资料来源如下

  • 网络

编程环境

  • 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


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

常用控件

TextView

  • TextView 显示文本信息
  • ```
    <TextView
      android:id="@+id/text_view"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:gravity="center"
      android:textSize="24sp"
      android:textColor="#00ff00"
      android:text="This is TextView" />
    
    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
    * 效果如下
    ![ScreenShot_20161209145323.png](https://i.loli.net/2019/03/26/5c9a25ffa6833.png)

    #### 常用属性详解
    * android:id 指定当前控件唯一标识符
    * android:layout_width android:layout_height
    指定控件的宽度和高度,取值有
    * match_parent 匹配父布局
    * wrap_content自适应内容
    * fill_parent与match_parent 相同

    * android:gravity 文字对齐方式,
    可取值 top bottom left right center 可以使用 | 同时使用多个属性

    * android:text 文本内容

    * android:textSize android:textColor 文本的大小/颜色

    ### Button
    * [Button](https://developer.android.com/reference/android/widget/Button.html) 按钮
    * 代码
    ```xml
    <Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Button"
    android:textAllCaps="false"/>
  • 效果如下
    2.png

常用属性

  • android:textAllCaps 系统自动转换文本为大写 true/false

按钮响应(3种)

  • 匿名类注册监听器

    • 代码如下
      1
      2
      3
      4
      5
      6
      7
      8
      //新建Button对象 //强制类型转换
      Button button = (Button)findViewById(R.id.button);
      button.setOnClickListener(new View.OnClickListener(){
      @Override
      public void onClick(View v){
      //添加逻辑
      }
      });
  • 接口方式实现

    • 代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
         //继承接口
      public class MainActivity extends AppCompatActivity implements View.OnClickListener{

      @Override
      protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      //注册监听器
      Button button = (Button)findViewById(R.id.button);
      button.setOnClickListener(this);
      }

      @Override
      //根据button id处理不同逻辑
      public void onClick(View view){
      switch (view.getId()){
      case R.id.button:
      //处理逻辑
      break;
      default:
      break;
      }
      }
      }
  • android:onClick 匹配
    在xml中指定android:onClick 指定的方法名称匹配,签名必须完全相同
    方法要求:

    • 是公共方法 public

    • 具有空返回值 void

    • 以 View 作为唯一参数(这将是之前点击的 View)

    • 代码如下

      1
      android:onClick="Button_onClick"
      1
      2
      3
      public void Button_onClick(View view){
      //处理逻辑
      }

      EditText

  • EditText

  • 代码如下

    1
    2
    3
    4
    5
    6
    <EditText
    android:id="@+id/edit_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="输入文字"
    android:maxLines="2"/>
  • 效果
    ScreenShot_20161210102606.png

    常用属性

  • android:hint 指定一些提示性文字,再用户未输入时提示。

  • android:maxLines 输入内容最大占用行数

提取输入文本

  • 通过findViewById找到EditText 实例,在处理逻辑中调用EditText.getText方法得到输入内容,再由toString转换为字符串。
  • 代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //新建EditTExt对象
    private EditText editText;

    //绑定EditText实例
    editText = (EditText)findViewById(R.id.edit_text);

    //提取文本
    String inputText = editText.getText().toString();
    //Toast显示
    Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();

ImageView

  • 将图片放入drawable-xhdpi文件夹
  • ImageView
  • 代码
    1
    2
    3
    4
    5
    <ImageView
    android:id="@+id/image_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/img_1"/>
  • 效果如下
    ScreenShot_20161211142408.png

常用属性

  • android:src 引用资源位置

更改ImageView图片

  • 调用 imageView.setImageResource()方法
  • 代码如下
    1
    2
    3
    4
    5
    private ImageView imageView;

    imageView = (ImageView)findViewById(R.id.image_view);

    imageView.setImageResource(R.drawable.img_2);

ProgressBar

  • ProgressBar
  • 代码如下
    1
    2
    3
    4
    5
    6
    <ProgressBar
    android:id="@+id/progress_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="?android:attr/progressBarStyleHorizontal"
    android:max="100"/>
  • ScreenShot_20161211143148.png

常用属性

  • android:visibility 是否可见 取值 visible invisible gone
  • style=”?android:attr/progressBarStyleHorizontal”
    android:max=”100”
    设定显示方式为横向进度条,进度条最大值100

进度条有关设置

  • 显示/隐藏进度条
    progressBar.setVisibility()方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private ProgressBar progressBar;

    progressBar = (ProgressBar)findViewById(R.id.progress_bar);

    //设置ProgressBar
    if(progressBar.getVisibility() == View.GONE){
    progressBar.setVisibility(View.VISIBLE);
    } else {
    progressBar.setVisibility(View.GONE);
    }

  • 设置进度条进度
    1
    2
    3
    int progress = progressBar.getProgress();
    progress +=10;
    progressBar.setProgress(progress);

AlertDialog

  • AlertDialog
  • 代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
    dialog.setTitle("This is Dialog");
    dialog.setMessage("something important");
    dialog.setCancelable(false);
    //setPositiveButton设定OK点击事件
    dialog.setPositiveButton("OK",new DialogInterface.OnClickListener(){
    @Override
    public void onClick(DialogInterface dialog,int which){
    }
    });
    //setNegativeButton设定Canncel点击事件
    dialog.setNegativeButton("Canncel",new DialogInterface.OnClickListener(){
    @Override
    public void onClick(DialogInterface dialog,int which){
    }
    });
    //显示 AlertDialog
    dialog.show();
  • ScreenShot_20161211195720.png

ProgressDialog

  • ProgressDialog与AlertDialog类似,但是会额外显示一个进度条

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //新建ProgressDialog对象
    ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
    //设置标题
    progressDialog.setTitle("This is ProgressDialog");
    //设置内容
    progressDialog.setMessage("Loading...");
    //是否可以返回键取消
    progressDialog.setCancelable(false);
    //显示
    progressDialog.show();
  • 效果如图
    ScreenShot_20161211200629.png

  • PS:progressDialog.setCancelable();属性设置为false时表示ProgressDialog无法通过Back键取消。只能通过progressDialog.dismiss()方法取消

四种基本布局

LinearLayout 线性布局

  • 线性方向上依次排列
  • 基础效果如下
    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
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ljy.com.uilayouttest.MainActivity">
    <Button
    android:id="@+id/button_1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button_1"/>

    <Button
    android:id="@+id/button_2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button_2"/>

    <Button
    android:id="@+id/button_3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button_3"/>

    </LinearLayout>

    基础属性

  • android:orientation LinearLayout 的排列方向,取值有两种 horizontal横向和vertical竖向,不指定 android:orientation时,默认 horizontal
  • android:layout_gravity _指定控件在布局中的对齐方式。该属性与 LinearLayout 的排列方向有很大关系。
  • android:layout_weight _允许使用比例方式指定控件大小,计算控件大小时,系统非将所有控件的android:layout_weight _值相加,当作基底,计算指定的大小比例
    • EditText和Button
      常见用法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
          <EditText
      android:id="@+id/button_1"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:hint="Button_1"/>

      <Button
      android:id="@+id/button_2"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Button_2"/>
      ScreenShot_20161213100420.png
      指定android:layout_width _为0dp,android:layout_weight=”1”均为1 _平分大小
    • 另一种用法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
          <EditText
      android:id="@+id/button_1"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:hint="Button_1"/>

      <Button
      android:id="@+id/button_2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Button_2"/>
      将Button的android:layout_width设定为wrap_content,EditText 的android:layout_weight=”1”,EditText会占满整个屏幕剩余部分,在适配屏幕时较常用_

RelativeLayout相对布局

  • 属性较多,以代码形式说明

    相对父布局位置

  • 代码如下
    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
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ljy.com.uilayouttest.MainActivity">

    <Button
    android:id="@+id/button_1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:text="Button 1"/>
    <Button
    android:id="@+id/button_2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    android:text="Button 2"/>
    <Button
    android:id="@+id/button_3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:text="Button 3"/>
    <Button
    android:id="@+id/button_4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_alignParentLeft="true"
    android:text="Button 4"/>
    <Button
    android:id="@+id/button_5"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_alignParentRight="true"
    android:text="Button 5"/>

    </RelativeLayout>
  • ScreenShot_20161213102007.png
  • android:layout_alignParentLeft=”true”
    android:layout_alignParentTop=”true”
    android:layout_centerInParent=”true”
    android:layout_alignParentBottom=”true”
    android:layout_alignParentRight=”true”
    _简而言之这些属性指定了控件相对父布局的位置

相对控件位置

  • 代码如下

    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
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ljy.com.uilayouttest.MainActivity">

    <Button
    android:id="@+id/button_3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:text="Button 3"/>
    <Button
    android:id="@+id/button_1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_above="@id/button_3"
    android:layout_toLeftOf="@id/button_3"
    android:text="Button 1"/>
    <Button
    android:id="@+id/button_2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_above="@id/button_3"
    android:layout_toRightOf="@id/button_3"
    android:text="Button 2"/>

    <Button
    android:id="@+id/button_4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/button_3"
    android:layout_toLeftOf="@id/button_3"
    android:text="Button 4"/>
    <Button
    android:id="@+id/button_5"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/button_3"
    android:layout_toRightOf="@id/button_3"
    android:text="Button 5"/>

    </RelativeLayout>
  • ScreenShot_20161213103016.png

  • 说明

    • android:layout_above 可以指定一个控件位于指定控件的上方,需要指定ID引用
    • android:layout_below 指定一个控件位于指定控件的下方,id引用
    • android:layout_toLeftOf 指定一个控件位于指定控件的左侧,id引用
    • android:layout_toRightOf 指定一个控件位于指定控件的右侧,id引用。
  • NOTE:当控件去引用另一个控件的ID时,引用控件一定要在前本例中是id/button_3在最前面_

FrameLayout帧布局

  • 所有控件默认左上角

  • 代码如下
    两个控件重合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
    android:id="@+id/text_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="TextView"/>
    <ImageView
    android:id="@+id/image_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/ic_launcher"/>
    </FrameLayout>

    ScreenShot_20161213113149.png

  • android:layout_gravity _同样可以应用于FrameLayout中,指定对齐方式

PercentFrameLayout百分比布局

  • PercentFrameLayout并非内置于系统SDK中,使用前要在build.gradle中添加百分比布局的依赖。
  • 修改app/build.gradle文件,在dependencies闭包中增加依赖。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.0.1'
    //下面一行为增加内容,要与上面的版本号保持一致
    compile 'com.android.support:percent:25.0.1'
    testCompile 'junit:junit:4.12'
    }
    修改完成后,as会开始同步,同步完成即可。
  • PercentFrameLayout继承了FrameLayout的特性,所有控件默认左上角,需要通过android:layout_gravity _来调整位置

  • 源码如下

    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
    # PercentFrameLayout 并非系统内置SDK,需要声明完整包路径
    <android.support.percent.PercentFrameLayout
    # 随后定义app的命名空间
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
    android:id="@+id/button1"
    android:text="Button1"
    android:layout_gravity="left|top"
    app:layout_widthPercent="50%"
    app:layout_heightPercent="50%"/>
    <Button
    android:id="@+id/button2"
    android:text="Button2"
    android:layout_gravity="right|top"
    app:layout_widthPercent="50%"
    app:layout_heightPercent="50%"/>
    <Button
    android:id="@+id/button3"
    android:text="Button3"
    android:layout_gravity="left|bottom"
    app:layout_widthPercent="50%"
    app:layout_heightPercent="50%"/>
    <Button
    android:id="@+id/button4"
    android:text="Button4"
    android:layout_gravity="right|bottom"
    app:layout_widthPercent="50%"
    app:layout_heightPercent="50%"/>

    </android.support.percent.PercentFrameLayout>
  • 效果如图
    ScreenShot_20161213145134.png

  • 与之类似的还有 PercentRelativeLayout,用法不加累赘

自定义控件

引入布局

  • 新建title.xml
    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
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/title_bg">

    <Button
    android:id="@+id/title_back"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="5dp"
    android:background="@drawable/back_bg"
    android:text="Back"
    android:textColor="#fff" />

    <TextView
    android:id="@+id/title_text"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_weight="1"
    android:gravity="center"
    android:text="Title Text"
    android:textColor="#fff"
    android:textSize="24sp" />

    <Button
    android:id="@+id/title_edit"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="5dp"
    android:background="@drawable/edit_bg"
    android:text="Edit"
    android:textColor="#fff" />

    </LinearLayout>
  • 在activity_main.xml _中使用title.xml
    <include layout="@layout/title"/>
  • 隐藏系统自带标题栏,在mainActivity的onCreat中添加如下代码。
    1
    2
    3
      ActionBar actionBar = getSupportActionBar();
    if (actionBar != null)
    actionBar.hide();
  • 效果如下
    ScreenShot_20161213182554.png

自定义控件

  • 创建TitleLayout继承自LinearLayout ,代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class TitleLayout extends LinearLayout {
    //构造函数
    public TitleLayout(Context context, AttributeSet attributeSet){
    super(context,attributeSet);
    //调用LayoutInflater.from方法构建LayoutInflater对象,
    //再调用inflate加载布局文件
    LayoutInflater.from(context).inflate(R.layout.title,this);
    }
    }
  • 再activity_main.xml _中 添加自定义控件,
    添加自定义控件时要指明控件的完整类名
    1
    2
    3
    <ljy.com.uicustomviews.TitleLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
  • 效果与引入布局文件相同

注册按钮点击事件

  • 在TitleLayout的构造函数添加按钮注册点击事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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通知
    Toast.makeText(getContext(),"clik",Toast.LENGTH_SHORT).show();
    }
    });