图片瘦身框架 Tiny

目的

为了简化对图片压缩的调用,提供最简洁与合理的api压缩逻辑,对于压缩为Bitmap根据屏幕分辨率动态适配最佳大小,对于压缩为File优化底层libjpeg的压缩,整个图片压缩过程全在压缩线程池中异步压缩,结束后分发回UI线程。

支持的压缩类型

Tiny图片压缩框架支持的压缩数据源类型:

1、Bytes 2、File 3、Bitmap 4、Stream 5、Resource 6、Uri(network、file、content)

Tiny支持单个数据源压缩以及批量压缩,支持的压缩类型:

1、数据源—>压缩为Bitmap 2、数据源—>压缩为File 3、数据源—>压缩为File并返回压缩后的Bitmap 4、批量数据源—>批量压缩为Bitmap 5、批量数据源—>批量压缩为File 6、批量数据源—>批量压缩为File并返回压缩后Bitmap

压缩参数

Tiny.BitmapCompressOptions

Bitmap压缩参数可配置三个:

1、width 2、height 3、Bitmap.Config

如果不配置,Tiny内部会根据屏幕动态适配以及默认使用ARGB_8888

Tiny.FileCompressOptions

File压缩参数可配置四个:

1、quality-压缩质量,默认为76 2、isKeepSampling-是否保持原数据源图片的宽高 3、fileSize-压缩后文件大小 4、outfile-压缩后文件存储路径

如果不配置,Tiny内部会根据默认压缩质量进行压缩,压缩后文件默认存储在:ExternalStorage/Android/data/${packageName}/tiny/目录下

接入

见项目README

压缩为Bitmap

  1. Tiny.BitmapCompressOptions options = new Tiny.BitmapCompressOptions();
  2. Tiny.getInstance().source("").asBitmap().withOptions(options).compress(new BitmapCallback() {
  3. @Override
  4. public void callback(boolean isSuccess, Bitmap bitmap) {
  5. //return the compressed bitmap object
  6. }
  7. });

压缩为File

  1. Tiny.FileCompressOptions options = new Tiny.FileCompressOptions();
  2. Tiny.getInstance().source("").asFile().withOptions(options).compress(new FileCallback() {
  3. @Override
  4. public void callback(boolean isSuccess, String outfile) {
  5. //return the compressed file path
  6. }
  7. });

压缩为File并返回Bitmap

  1. Tiny.FileCompressOptions options = new Tiny.FileCompressOptions();
  2. Tiny.getInstance().source("").asFile().withOptions(options).compress(new FileWithBitmapCallback() {
  3. @Override
  4. public void callback(boolean isSuccess, Bitmap bitmap, String outfile) {
  5. //return the compressed file path and bitmap object
  6. }
  7. });

批量压缩为Bitmap

  1. Tiny.BitmapCompressOptions options = new Tiny.BitmapCompressOptions();
  2. Tiny.getInstance().source("").batchAsBitmap().withOptions(options).batchCompress(new BitmapBatchCallback() {
  3. @Override
  4. public void callback(boolean isSuccess, Bitmap[] bitmaps) {
  5. //return the batch compressed bitmap object
  6. }
  7. });

批量压缩为File

  1. Tiny.FileCompressOptions options = new Tiny.FileCompressOptions();
  2. Tiny.getInstance().source("").batchAsFile().withOptions(options).batchCompress(new FileBatchCallback() {
  3. @Override
  4. public void callback(boolean isSuccess, String[] outfile) {
  5. //return the batch compressed file path
  6. }
  7. });

批量压缩为File并返回Bitmap

  1. Tiny.FileCompressOptions options = new Tiny.FileCompressOptions();
  2. Tiny.getInstance().source("").batchAsFile().withOptions(options).batchCompress(new FileWithBitmapBatchCallback() {
  3. @Override
  4. public void callback(boolean isSuccess, Bitmap[] bitmaps, String[] outfile) {
  5. //return the batch compressed file path and bitmap object
  6. }
  7. });

Tiny与微信朋友圈的压缩率比较

下面是使用Tiny图片压缩库进行压缩的效果对比示例:
| 图片信息 | Tiny | Wechat |
| :-: | :-: | :-: | :-: |
| 6.66MB (3500x2156) | 151KB (1280x788) | 135KB (1280x789) |
| 4.28MB (4160x3120) | 219KB (1280x960) | 195KB (1280x960) |
| 2.60MB (4032x3024) | 193KB (1280x960)) | 173KB (1280x960) |
| 372KB (500x500) | 38.67KB (500x500) | 34.05KB (500x500) |
| 236KB (960x1280) | 127KB (960x1280) | 118KB (960x1280) |

也谈图片压缩

Android运行时Crash自动恢复框架-Recovery

简介

App Crash的恢复,这个想法很早之前就有,目前有些时间就实现了一把,主要是对App运行时发生Crash后,对Activity的堆栈和数据进行恢复,或者重启应用,或者重启并清空缓存,避免因本地的数据类型或格式错误而导致App在读取时一直Crash,Debug模式还包括Crash信息的显示和保存,便于在开发、测试时查看相应CrashInfo

Crash的处理

对于应用的Crash,一般的做法我们往往都是实现个自定义UncaughtExceptionHandler,而这个自定义的CustomUncaughtHandler我们一般都用于捕捉Crash信息进行上报和监控是否发生Crash,还有一个作用就是可以屏蔽系统默认的Crash对话框,也就是拦截Crash后不把系统默认的UncaughtHandler设置进去,而是直接进行KillProcess,这个过程就是屏蔽了系统的默认Crash处理流程,原因是系统的处理其中在AMS的crashApplication()中会执行这么一段代码:

  1. Message msg = Message.obtain();
  2. msg.what = SHOW_ERROR_MSG;
  3. HashMap data = new HashMap();
  4. data.put("result", result);
  5. data.put("app", r);
  6. msg.obj = data;
  7. mUiHandler.sendMessage(msg);

发送一个显示Dialog的消息,之后便创建一个AppErrorDialog进行显示。

当然还有另外一种屏蔽系统默认ErrorDialog的方法,就是对AMP进行Hook,拦截handleApplicationCrash()方法后进行KillProcess,这样的话永远都将不会出现系统默认对话框,即使把系统默认的设置进去了,这个方法建议App内对AMP进行了Hook的做,其它App反而只为实现这个小功能而进行Hook成本太高,还是用自定义的做法进行屏蔽。

Recovery

Crash处理流程

对于Recovery,在应用发生Crash时,会进入一个Recovery界面,在该界面可以进行界面的恢复、应用的重启,或进入debug模式进行Crash信息的查看与保存

接入

请戳这里

RecoveryActivity

在应用发生Crash后,将进入RecoveryActivity界面

ActivityStack的恢复

对于恢复界面,默认是恢复整个Activity的堆栈,以便保护用户之前的数据
当应用在前台时崩溃无非就三种:
1、界面一创建就崩溃,可能在onCreate等方法中读取数据造成的Crash
2、界面创建且绘制完成正常显示,在用户执行某个操作,如点击按钮执行某个操作等造成的Crash
3、其它异步线程、服务等在后台执行任务时导致的Crash
上面的情况都应恢复绘制完成后的界面,也就是栈顶Activity是在Crash之前用户所看到的界面,而之前创建且未销毁的Activity也应该进行恢复。
当应用在后台时:
1、进程未挂,无非就是异步线程、server等后台任务发生异常时导致的Crash
2、进程已挂,进程被360等工具杀死了,常见的是push过来了然后唤起App进程,在解析push信息时候导致Crash
上面的情况App在后台时导致的Crash,Recovery提供了一个参数(recoverInBackgroud),用来设置是否在后台Crash时进行恢复。
ActivityStack恢复的操作,都是先恢复栈中的Activity,无Activity时则重启应用

主页的回退

在进行恢复Activity时,如果只是恢复栈顶Activity,当用户在这个界面不进行跳转操作而是直接按返回键,这将导致直接退出程序,所以对于这个情况应该是回退到应用的主页,Recovery中有个参数mainPage,如果设置了就表示需要回退到主页,没有设置则不进行回退
这个过程中涉及到获取App内Activity栈内的数量和栈底Activity,是开发人员应该都知道获取这两个信息是通过getRunningTasks来获取,不过可惜,在5.0以后Google对权限进行了收敛,目地是保护App的信息安全,这个方法在5.0以后将失效,所以需要另外一种方法进行兼容,于是乎看6.0源码又发现Google在5.0收敛了整个权限,导致本App的都获取不到,但是在6.0又放出来了,不过只能获取本应用的数据,所以兼容的策略是5.0~6.0自己维护一个ActivityStack

连续Crash的处理

如果一分钟内进行了两次恢复后还导致Crash,则不进行恢复而是重启应用,或者重启并清空缓存,以便恢复App刚安装时的状态

静默恢复

对于应用运行时发生Crash后的恢复,默认是显示RecoveryActivity,也就是上图的界面来让用户选择是否需要进行恢复,同时也支持静默恢复,也就是不显示界面,在发生Crash后根据所配置参数自动的恢复(重启、恢复ActivityStack、恢复栈顶Activity、重启并清空缓存)

无图言屌

蓝牙快速开发框架 FastBle

因为自己的项目中有用到了蓝牙相关的功能,所以之前也断断续续地针对蓝牙通信尤其是BLE通信进行了一番探索,整理出了一个开源框架FastBle与各位分享经验。
源码地址:

https://github.com/Jasonchenlijian/FastBle

随着对FastBle框架关注的人越来越多,与我讨论问题的小伙伴也多起来,所以整理了一篇文章,详细介绍一下框架的用法,一些坑,还有我对Android BLE开发实践方面的理解。
文章分为3个部分:

  • FastBle的使用
  • BLE开发实践方面的理解
  • FastBle源码解析

1. FastBle的使用

1.1 声明权限

  1. <uses-permission android:name="android.permission.BLUETOOTH" />
  2. <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  3. <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  4. <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  • android.permission.BLUETOOTH : 这个权限允许程序连接到已配对的蓝牙设备, 请求连接/接收连接/传输数据需要改权限, 主要用于对配对后进行操作;
  • android.permission.BLUETOOTH_ADMIN : 这个权限允许程序发现和配对蓝牙设备, 该权限用来管理蓝牙设备, 有了这个权限, 应用才能使用本机的蓝牙设备, 主要用于对配对前的操作;
  • android.permission.ACCESS_COARSE_LOCATION和android.permission.ACCESS_FINE_LOCATION:Android 6.0以后,这两个权限是必须的,蓝牙扫描周围的设备需要获取模糊的位置信息。这两个权限属于同一组危险权限,在清单文件中声明之后,还需要再运行时动态获取。

    1.2. 初始化及配置

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5. BleManager.getInstance().init(getApplication());
  6. BleManager.getInstance()
  7. .enableLog(true)
  8. .setReConnectCount(1, 5000)
  9. .setOperateTimeout(5000);
  10. }

在使用之前,需要事先调用初始化init(Application app)方法。此外,可以进行一些自定义的配置,比如是否显示框架内部日志,重连次数和重连时间间隔,以及操作超时时间。

1.3. 扫描外围设备

APP作为中心设备,想要与外围硬件设备建立蓝牙通信的前提是首先得到设备对象,途径是扫描。在调用扫描方法之前,你首先应该先处理下面的准备工作。

  • 判断当前Android设备是否支持BLE。
    Android 4.3以后系统中加入了蓝牙BLE的功能。

    1. BleManager.getInstance().isSupportBle();
  • 判断当前Android设备的蓝牙是否已经打开。
    可以直接调用下面的判断方法来判断本机是否已经打开了蓝牙,如果没有,向用户抛出提示。

    1. BleManager.getInstance().isBlueEnable();
  • 主动打开蓝牙。
    除了判断蓝牙是否打开给以用户提示之外,我们也可以通过程序直接帮助用户打开蓝牙开关,打开方式有这几种:
    方法1:通过蓝牙适配器直接打开蓝牙。

    1. BleManager.getInstance().enableBluetooth();
  • 方法2:通过startActivityForResult引导界面引导用户打开蓝牙。

    1. Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    2. startActivityForResult(intent, 0x01);
  • 需要注意的是,第一种方法是异步的,打开蓝牙需要一段时间,调用此方法后,蓝牙不会立刻就处于开启状态。如果使用此方法后紧接者就需要进行扫描,建议维护一个阻塞线程,内部每隔一段时间查询蓝牙是否处于开启状态,外部显示等待UI引导用户等待,直至开启成功。使用第二种方法,会通过系统弹出框的形式引导用户开启,最终通过onActivityResult的形式回调通知是否开启成功。

  • 6.0及以上机型动态获取位置权限。
    蓝牙打开之后,进行扫描之前,需要判断下当前设备是否是6.0及以上,如果是,需要动态获取之前在Manifest中声明的位置权限。
  • 配置扫描规则
    扫描规则可以配置1个或多个,也可以不配置使用默认(扫描10秒)。扫描的时候,会根据配置的过滤选项,对扫描到的设备进行过滤,结果返回过滤后的设备。扫描时间配置为小于等于0,会实现无限扫描,直至调用BleManger.getInstance().cancelScan()来中止扫描。

    1. BleScanRuleConfig scanRuleConfig = new BleScanRuleConfig.Builder()
    2. .setServiceUuids(serviceUuids) // 只扫描指定的服务的设备,可选
    3. .setDeviceName(true, names) // 只扫描指定广播名的设备,可选
    4. .setDeviceMac(mac) // 只扫描指定mac的设备,可选
    5. .setAutoConnect(isAutoConnect) // 连接时的autoConnect参数,可选,默认false
    6. .setScanTimeOut(10000) // 扫描超时时间,可选,默认10秒
    7. .build();
    8. BleManager.getInstance().initScanRule(scanRuleConfig);
  • 以上准备工作完成后,就可以开始进行扫描。

    1. BleManager.getInstance().scan(new BleScanCallback() {
    2. @Override
    3. public void onScanStarted(boolean success) {
    4. }
    5. @Override
    6. public void onLeScan(BleDevice bleDevice) {
    7. }
    8. @Override
    9. public void onScanning(BleDevice bleDevice) {
    10. }
    11. @Override
    12. public void onScanFinished(List<BleDevice> scanResultList) {
    13. }
    14. });

    onScanStarted(boolean success): 会回到主线程,参数表示本次扫描动作是否开启成功。由于蓝牙没有打开,上一次扫描没有结束等原因,会造成扫描开启失败。
    onLeScan(BleDevice bleDevice):扫描过程中所有被扫描到的结果回调。由于扫描及过滤的过程是在工作线程中的,此方法也处于工作线程中。同一个设备会在不同的时间,携带自身不同的状态(比如信号强度等),出现在这个回调方法中,出现次数取决于周围的设备量及外围设备的广播间隔。
    onScanning(BleDevice bleDevice):扫描过程中的所有过滤后的结果回调。与onLeScan区别之处在于:它会回到主线程;同一个设备只会出现一次;出现的设备是经过扫描过滤规则过滤后的设备。
    onScanFinished(List<BleDevice> scanResultList):本次扫描时段内所有被扫描且过滤后的设备集合。它会回到主线程,相当于onScanning设备之和。

    1.4. 设备信息

    扫描得到的BLE外围设备,会以BleDevice对象的形式,作为后续操作的最小单元对象。它本身含有这些信息:
    String getName():蓝牙广播名
    String getMac():蓝牙Mac地址
    byte[] getScanRecord(): 被扫描到时候携带的广播数据
    int getRssi() :被扫描到时候的信号强度
    后续进行设备连接、断开、判断设备状态,读写操作等时候,都会用到这个对象。可以把它理解为外围蓝牙设备的载体,所有对外围蓝牙设备的操作,都通过这个对象来传导。

    1.5. 连接、断连、监控连接状态

    拿到设备对象之后,可以进行连接操作。

  1. BleManager.getInstance().connect(bleDevice, new BleGattCallback() {
  2. @Override
  3. public void onStartConnect() {
  4. }
  5. @Override
  6. public void onConnectFail(BleException exception) {
  7. }
  8. @Override
  9. public void onConnectSuccess(BleDevice bleDevice, BluetoothGatt gatt, int status) {
  10. }
  11. @Override
  12. public void onDisConnected(boolean isActiveDisConnected, BleDevice bleDevice, BluetoothGatt gatt, int status) {
  13. }
  14. });

onStartConnect():开始进行连接。
onConnectFail(BleException exception):连接不成功。
onConnectSuccess(BleDevice bleDevice, BluetoothGatt gatt, int status):连接成功并发现服务。
onDisConnected(boolean isActiveDisConnected, BleDevice bleDevice, BluetoothGatt gatt, int status):连接断开,特指连接后再断开的情况。在这里可以监控设备的连接状态,一旦连接断开,可以根据自身情况考虑对BleDevice对象进行重连操作。需要注意的是,断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况。此外,如果通过调用disconnect(BleDevice bleDevice)方法,主动断开蓝牙连接的结果也会在这个方法中回调,此时isActiveDisConnected将会是true。

1.6. GATT协议

BLE连接都是建立在 GATT (Generic Attribute Profile) 协议之上。GATT 是一个在蓝牙连接之上的发送和接收很短的数据段的通用规范,这些很短的数据段被称为属性(Attribute)。它定义两个 BLE 设备通过Service 和 Characteristic 进行通信。GATT 就是使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service, Characteristic以及对应的数据保存在一个查找表中,次查找表使用 16 bit ID 作为每一项的索引。
关于GATT这部分内容会在下面重点讲解。总之,中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。当连接成功之后,外围设备与中心设备之间就建立起了GATT连接。
上面讲到的connect(BleDevice bleDevice, BleGattCallback bleGattCallback)方法其实是有返回值的,这个返回值就是BluetoothGatt。当然还有其他方式可以获取BluetoothGatt对象,连接成功后,调用:

  1. BluetoothGatt gatt = BleManager.getInstance().getBluetoothGatt(BleDevice bleDevice);

通过BluetoothGatt对象作为连接桥梁,中心设备可以获取外围设备的很多信息,以及双向通信。
首先,就可以获取这个蓝牙设备所拥有的Service和Characteristic。每一个属性都可以被定义作不同的用途,通过它们来进行协议通信。下面的方法,就是通过BluetoothGatt,查找出所有的Service和Characteristic的UUID:

  1. List<BluetoothGattService> serviceList = bluetoothGatt.getServices();
  2. for (BluetoothGattService service : serviceList) {
  3. UUID uuid_service = service.getUuid();
  4. List<BluetoothGattCharacteristic> characteristicList= service.getCharacteristics();
  5. for(BluetoothGattCharacteristic characteristic : characteristicList) {
  6. UUID uuid_chara = characteristic.getUuid();
  7. }
  8. }

1.7. 协议通信

APP与设备建立了连接,并且知道了Service和Characteristic(需要与硬件协议沟通确认)之后,我们就可以通过BLE协议进行通信了。通信的桥梁,主要就是是通过 标准的或者自定义的Characteristic,中文我们称之为“特征”。我们可以从 Characteristic 读数据和写数据。这样就实现了双向的通信。站在APP作为中心设备的角度,常用于数据交互的通信方式主要有3种:接收通知、写、读,此外还有设置最大传输单元,获取实时信号强度等通信操作。

  • 接收通知
    有两种方式可以接收通知,indicate和notify。indicate和notify的区别就在于,indicate是一定会收到数据,notify有可能会丢失数据。indicate底层封装了应答机制,如果没有收到中央设备的回应,会再次发送直至成功;而notify不会有central收到数据的回应,可能无法保证数据到达的准确性,优势是速度快。通常情况下,当外围设备需要不断地发送数据给APP的时候,比如血压计在测量过程中的压力变化,胎心仪在监护过程中的实时数据传输,这种频繁的情况下,优先考虑notify形式。当只需要发送很少且很重要的一条数据给APP的时候,优先考虑indicate形式。当然,从Android开发角度的出发,如果硬件放已经考虑了成熟的协议和发送方式,我们需要做的仅仅是根据其配置的数据发送方式进行相应的对接即可。
    打开notify

    1. BleManager.getInstance().notify(
    2. bleDevice,
    3. uuid_service,
    4. uuid_characteristic_notify,
    5. new BleNotifyCallback() {
    6. @Override
    7. public void onNotifySuccess() {
    8. // 打开通知操作成功
    9. }
    10. @Override
    11. public void onNotifyFailure(BleException exception) {
    12. // 打开通知操作失败
    13. }
    14. @Override
    15. public void onCharacteristicChanged(byte[] data) {
    16. // 打开通知后,设备发过来的数据将在这里出现
    17. }
    18. });
  • 关闭notify

    1. BleManager.getInstance().stopNotify(uuid_service, uuid_characteristic_notify);
  • 打开indicate

    1. BleManager.getInstance().indicate(
    2. bleDevice,
    3. uuid_service,
    4. uuid_characteristic_indicate,
    5. new BleIndicateCallback() {
    6. @Override
    7. public void onIndicateSuccess() {
    8. // 打开通知操作成功
    9. }
    10. @Override
    11. public void onIndicateFailure(BleException exception) {
    12. // 打开通知操作失败
    13. }
    14. @Override
    15. public void onCharacteristicChanged(byte[] data) {
    16. // 打开通知后,设备发过来的数据将在这里出现
    17. }
    18. });
  • 关闭indicate

    1. BleManager.getInstance().stopIndicate(uuid_service, uuid_characteristic_indicate);
  • 这里的通知操作用到了两个关键的参数,uuid_serviceuuid_characteristic_notify(或uuid_characteristic_indicate),就是上面提到的Service和Characteristic,此处以字符串的形式体现,不区分大小写。

  • 读写

    1. BleManager.getInstance().read(
    2. bleDevice,
    3. uuid_service,
    4. uuid_characteristic_read,
    5. new BleReadCallback() {
    6. @Override
    7. public void onReadSuccess(byte[] data) {
    8. // 读特征值数据成功
    9. }
    10. @Override
    11. public void onReadFailure(BleException exception) {
    12. // 读特征值数据失败
    13. }
    14. });
    15. BleManager.getInstance().write(
    16. bleDevice,
    17. uuid_service,
    18. uuid_characteristic_write,
    19. data,
    20. new BleWriteCallback() {
    21. @Override
    22. public void onWriteSuccess(int current, int total, byte[] justWrite) {
    23. // 发送数据到设备成功(分包发送的情况下,可以通过方法中返回的参数可以查看发送进度)
    24. }
    25. @Override
    26. public void onWriteFailure(BleException exception) {
    27. // 发送数据到设备失败
    28. }
    29. });
  • 进行BLE数据相互发送的时候,一次最多能发送20个字节。如果需要发送的数据超过20个字节,有两种方法,一种是主动尝试拓宽MTU,另一种是采用分包传输的方式。框架中的write方法,当遇到数据超过20字节的情况时,默认是进行分包发送的。

  • 设置最大传输单元MTU

    1. BleManager.getInstance().setMtu(bleDevice, mtu, new BleMtuChangedCallback() {
    2. @Override
    3. public void onSetMTUFailure(BleException exception) {
    4. // 设置MTU失败
    5. }
    6. @Override
    7. public void onMtuChanged(int mtu) {
    8. // 设置MTU成功,并获得当前设备传输支持的MTU值
    9. }
    10. });
  • 获取设备的实时信号强度Rssi

    1. BleManager.getInstance().readRssi(
    2. bleDevice,
    3. new BleRssiCallback() {
    4. @Override
    5. public void onRssiFailure(BleException exception) {
    6. // 读取设备的信号强度失败
    7. }
    8. @Override
    9. public void onRssiSuccess(int rssi) {
    10. // 读取设备的信号强度成功
    11. }
    12. });

    在BLE设备通信过程中,有几点经验分享给大家:

  • 两次操作之间最好间隔一小段时间,如100ms(具体时间可以根据自己实际蓝牙外设自行尝试延长或缩短)。举例,onConnectSuccess之后,延迟100ms再进行notify,之后再延迟100ms进行write

  • 连接及连接后的过程中,时刻关注onDisConnected方法,然后做处理。
  • 断开后如果需要重连,也请延迟一段时间,否则会造成阻塞。

2. BLE开发实践方面的理解

在分解FastBle源码之前,我首先介绍一下BLE通信一些理论知识。

2.1 蓝牙简介

蓝牙是一种近距离无线通信技术。它的特性就是近距离通信,典型距离是 10 米以内,传输速度最高可达 24 Mbps,支持多连接,安全性高,非常适合用智能设备上。

2.2 蓝牙技术的版本演进

  • 1999年发布1.0版本,目前市面上已很少见到;
  • 2002年发布1.1版本,目前市面上已很少见到;
  • 2004年发布2.0版本,目前市面上已很少见到;
  • 2007年发布的2.1版本,是之前使用最广的,也是我们所谓的经典蓝牙。
  • 2009年推出蓝牙 3.0版本,也就是所谓的高速蓝牙,传输速率理论上可高达24 Mbit/s;
  • 2010年推出蓝牙4.0版本,它是相对之前版本的集大成者,它包括经典蓝牙、高速蓝牙和蓝牙低功耗协议。经典蓝牙包括旧有蓝牙协议,高速蓝牙基于Wi-Fi,低功耗蓝牙就是BLE。
  • 2016年蓝牙技术联盟提出了新的蓝牙技术标准,即蓝牙5.0版本。蓝牙5.0针对低功耗设备速度有相应提升和优化,结合wifi对室内位置进行辅助定位,提高传输速度,增加有效工作距离,主要是针对物联网方向的改进。

    2.3 Android上BLE功能的逐步演进

    在Android开发过程中,版本的碎片化一直是需要考虑的问题,再加上厂商定制及蓝牙本身也和Android一样一直在发展过程中,所以对于每一个版本支持什么功能,是我们需要知道的。

  • Android 4.3 开始,开始支持BLE功能,但只支持Central Mode(中心模式)

  • Android 5.0开始,开始支持Peripheral Mode(外设模式)

中心模式和外设模式是什么意思?

  • Central Mode: Android端作为中心设备,连接其他外围设备。
  • Peripheral Mode:Android端作为外围设备,被其他中心设备连接。在Android 5.0支持外设模式之后,才算实现了两台Android手机通过BLE进行相互通信。

    2.4 蓝牙的广播和扫描

    以下内容部分参考自BLE Introduction
    关于这部分内容,需要引入一个概念,GAP(Generic Access Profile),它用来控制设备连接和广播。GAP 使你的设备被其他设备可见,并决定了你的设备是否可以或者怎样与设备进行交互。例如 Beacon 设备就只是向外发送广播,不支持连接;小米手环就可以与中心设备建立连接。
    在 GAP 中蓝牙设备可以向外广播数据包,广播包分为两部分: Advertising Data Payload(广播数据)和 Scan Response Data Payload(扫描回复),每种数据最长可以包含 31 byte。这里广播数据是必需的,因为外设必需不停的向外广播,让中心设备知道它的存在。扫描回复是可选的,中心设备可以向外设请求扫描回复,这里包含一些设备额外的信息,例如设备的名字。在 Android 中,系统会把这两个数据拼接在一起,返回一个 62 字节的数组。这些广播数据可以自己手动去解析,在 Android 5.0 也提供 ScanRecord 帮你解析,直接可以通过这个类获得有意义的数据。广播中可以有哪些数据类型呢?设备连接属性,标识设备支持的 BLE 模式,这个是必须的。设备名字,设备包含的关键 GATT service,或者 Service data,厂商自定义数据等等。

安卓框架整理 - 图1
广播流程

外围设备会设定一个广播间隔,每个广播间隔中,它会重新发送自己的广播数据。广播间隔越长,越省电,同时也不太容易扫描到。
刚刚讲到,GAP决定了你的设备怎样与其他设备进行交互。答案是有2种方式:

  • 完全基于广播的方式
    也有些情况是不需要连接的,只要外设广播自己的数据即可。用这种方式主要目的是让外围设备,把自己的信息发送给多个中心设备。使用广播这种方式最典型的应用就是苹果的 iBeacon。这是苹果公司定义的基于 BLE 广播实现的功能,可以实现广告推送和室内定位。这也说明了,APP 使用 BLE,需要定位权限。
    基于非连接的,这种应用就是依赖 BLE 的广播,也叫作 Beacon。这里有两个角色,发送广播的一方叫做 Broadcaster,监听广播的一方叫 Observer。
  • 基于GATT连接的方式大部分情况下,外设通过广播自己来让中心设备发现自己,并建立 GATT 连接,从而进行更多的数据交换。这里有且仅有两个角色,发起连接的一方,叫做中心设备—Central,被连接的设备,叫做外设—Peripheral。
    • 外围设备:这一般就是非常小或者简单的低功耗设备,用来提供数据,并连接到一个更加相对强大的中心设备,例如小米手环。
    • 中心设备:中心设备相对比较强大,用来连接其他外围设备,例如手机等。
  • GATT 连接需要特别注意的是:GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当设备断开,它又开始广播。中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。
    GATT 通信的双方是 C/S 关系。外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 Server 发起请求。需要注意的是,所有的通信事件,都是由客户端发起,并且接收服务端的响应。

    2.5 BLE通信基础

    BLE通信的基础有两个重要的概念,ATT和GATT。

  • ATT
    全称 attribute protocol,中文名“属性协议”。它是 BLE 通信的基础。ATT 把数据封装,向外暴露为“属性”,提供“属性”的为服务端,获取“属性”的为客户端。ATT 是专门为低功耗蓝牙设计的,结构非常简单,数据长度很短。

  • GATT
    全称 Generic Attribute Profile, 中文名“通用属性配置文件”。它是在ATT 的基础上,对 ATT 进行的进一步逻辑封装,定义数据的交互方式和含义。GATT是我们做 BLE 开发的时候直接接触的概念。
  • GATT 层级
    GATT按照层级定义了4个概念:配置文件(Profile)、服务(Service)、特征(Characteristic)和描述(Descriptor)。他们的关系是这样的:Profile 就是定义了一个实际的应用场景,一个 Profile包含若干个 Service,一个 Service 包含若干个 Characteristic,一个 Characteristic 可以包含若干 Descriptor。

安卓框架整理 - 图2
GATT层级

  • Profile
    Profile 并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。例如心率Profile(Heart Rate Profile)就是结合了 Heart Rate Service 和 Device Information Service。所有官方通过 GATT Profile 的列表可以从这里找到。
  • Service
    Service 是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。 UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,这个就可以自己随便设置。官方通过了一些标准 Service,完整列表在这里。以 Heart Rate Service为例,可以看到它的官方通过 16 bit UUID 是 0x180D,包含 3 个 Characteristic:Heart Rate Measurement, Body Sensor LocationHeart Rate Control Point,并且定义了只有第一个是必须的,它是可选实现的。
  • Characteristic
    需要重点提一下Characteristic, 它定义了数值和操作,包含一个Characteristic声明、Characteristic属性、值、值的描述(Optional)。通常我们讲的 BLE 通信,其实就是对 Characteristic 的读写或者订阅通知。比如在实际操作过程中,我对某一个Characteristic进行读,就是获取这个Characteristic的value。
  • UUID
    Service、Characteristic 和 Descriptor 都是使用 UUID 唯一标示的。
    UUID 是全局唯一标识,它是 128bit 的值,为了便于识别和阅读,一般以 “8位-4位-4位-4位-12位”的16进制标示,比如“12345678-abcd-1000-8000-123456000000”。
    但是,128bit的UUID 太长,考虑到在低功耗蓝牙中,数据长度非常受限的情况,蓝牙又使用了所谓的 16 bit 或者 32 bit 的 UUID,形式如下:“0000XXXX-0000-1000-8000-00805F9B34FB”。除了 “XXXX” 那几位以外,其他都是固定,所以说,其实 16 bit UUID 是对应了一个 128 bit 的 UUID。这样一来,UUID 就大幅减少了,例如 16 bit UUID只有有限的 65536(16的四次方) 个。与此同时,因为数量有限,所以 16 bit UUID 并不能随便使用。蓝牙技术联盟已经预先定义了一些 UUID,我们可以直接使用,比如“00001011-0000-1000-8000-00805F9B34FB”就一个是常见于BLE设备中的UUID。当然也可以花钱定制自定义的UUID。

3. FastBle源码解析

通过上面BLE的基础理论,我们可以分析到,BLE通信实际上就是先由客户端发起与服务端的连接,再通过服务端的找到其Characteristic进行两者间的数据交互。
在FastBle源码中,首先看BleManager中的connect()方法:

  1. public BluetoothGatt connect(BleDevice bleDevice, BleGattCallback bleGattCallback) {
  2. if (bleGattCallback == null) {
  3. throw new IllegalArgumentException("BleGattCallback can not be Null!");
  4. }
  5. if (!isBlueEnable()) {
  6. BleLog.e("Bluetooth not enable!");
  7. bleGattCallback.onConnectFail(new OtherException("Bluetooth not enable!"));
  8. return null;
  9. }
  10. if (Looper.myLooper() == null || Looper.myLooper() != Looper.getMainLooper()) {
  11. BleLog.w("Be careful: currentThread is not MainThread!");
  12. }
  13. if (bleDevice == null || bleDevice.getDevice() == null) {
  14. bleGattCallback.onConnectFail(new OtherException("Not Found Device Exception Occurred!"));
  15. } else {
  16. BleBluetooth bleBluetooth = new BleBluetooth(bleDevice);
  17. boolean autoConnect = bleScanRuleConfig.isAutoConnect();
  18. return bleBluetooth.connect(bleDevice, autoConnect, bleGattCallback);
  19. }
  20. return null;
  21. }

这个方法将扫描到的外围设备对象传入,通过一些必要的条件判断之后,调用bleBluetooth.connect()进行连接。我们去看一下BleBluetooth这个类:

  1. public BleBluetooth(BleDevice bleDevice) {
  2. this.bleDevice = bleDevice;
  3. }

上面的BleBluetooth的构造方法是传入一个蓝牙设备对象。由此可见,一个BleBluetooth可能代表你的Android与这一个外围设备整个交互过程,从开始连接,到中间数据交互,一直到断开连接的整个过程。在多连接情况下,有多少外围设备,设备池中就维护着多少个BleBluetooth对象。
MultipleBluetoothController就是控制多设备连接的。它里面有增加和移除设备的方法,如下图的addBleBluetoothremoveBleBluetooth,传入的参数就是BleBluetooth对象,验证了上面的说法。

  1. public synchronized void addBleBluetooth(BleBluetooth bleBluetooth) {
  2. if (bleBluetooth == null) {
  3. return;
  4. }
  5. if (!bleLruHashMap.containsKey(bleBluetooth.getDeviceKey())) {
  6. bleLruHashMap.put(bleBluetooth.getDeviceKey(), bleBluetooth);
  7. }
  8. }
  9. public synchronized void removeBleBluetooth(BleBluetooth bleBluetooth) {
  10. if (bleBluetooth == null) {
  11. return;
  12. }
  13. if (bleLruHashMap.containsKey(bleBluetooth.getDeviceKey())) {
  14. bleLruHashMap.remove(bleBluetooth.getDeviceKey());
  15. }
  16. }

回到BleBlutoothconnect方法:

  1. public synchronized BluetoothGatt connect(BleDevice bleDevice,
  2. boolean autoConnect,
  3. BleGattCallback callback) {
  4. addConnectGattCallback(callback);
  5. isMainThread = Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper();
  6. BluetoothGatt gatt;
  7. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  8. gatt = bleDevice.getDevice().connectGatt(BleManager.getInstance().getContext(),
  9. autoConnect, coreGattCallback, TRANSPORT_LE);
  10. } else {
  11. gatt = bleDevice.getDevice().connectGatt(BleManager.getInstance().getContext(),
  12. autoConnect, coreGattCallback);
  13. }
  14. if (gatt != null) {
  15. if (bleGattCallback != null)
  16. bleGattCallback.onStartConnect();
  17. connectState = BleConnectState.CONNECT_CONNECTING;
  18. }
  19. return gatt;
  20. }

可见,最终也是调用了原生API中的BluetoothDeviceconnectGatt()方法。在蓝牙原理分析中讲到,连接过程中要创建一个BluetoothGattCallback,用来作为回调,这个类非常重要,所有的 GATT 操作的回调都在这里。而此处的coreGattCallback应该就扮演着这个角色,它是BluetoothGattCallback的实现类对象,对操作回调结果做了封装和分发。

  1. private BluetoothGattCallback coreGattCallback = new BluetoothGattCallback() {
  2. @Override
  3. public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
  4. super.onConnectionStateChange(gatt, status, newState);
  5. if (newState == BluetoothGatt.STATE_CONNECTED) {
  6. gatt.discoverServices();
  7. } else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
  8. closeBluetoothGatt();
  9. BleManager.getInstance().getMultipleBluetoothController().removeBleBluetooth(BleBluetooth.this);
  10. if (connectState == BleConnectState.CONNECT_CONNECTING) {
  11. connectState = BleConnectState.CONNECT_FAILURE;
  12. if (isMainThread) {
  13. Message message = handler.obtainMessage();
  14. message.what = BleMsg.MSG_CONNECT_FAIL;
  15. message.obj = new BleConnectStateParameter(bleGattCallback, gatt, status);
  16. handler.sendMessage(message);
  17. } else {
  18. if (bleGattCallback != null)
  19. bleGattCallback.onConnectFail(new ConnectException(gatt, status));
  20. }
  21. } else if (connectState == BleConnectState.CONNECT_CONNECTED) {
  22. connectState = BleConnectState.CONNECT_DISCONNECT;
  23. if (isMainThread) {
  24. Message message = handler.obtainMessage();
  25. message.what = BleMsg.MSG_DISCONNECTED;
  26. BleConnectStateParameter para = new BleConnectStateParameter(bleGattCallback, gatt, status);
  27. para.setAcitive(isActiveDisconnect);
  28. para.setBleDevice(getDevice());
  29. message.obj = para;
  30. handler.sendMessage(message);
  31. } else {
  32. if (bleGattCallback != null)
  33. bleGattCallback.onDisConnected(isActiveDisconnect, bleDevice, gatt, status);
  34. }
  35. }
  36. }
  37. }
  38. @Override
  39. public void onServicesDiscovered(BluetoothGatt gatt, int status) {
  40. super.onServicesDiscovered(gatt, status);
  41. BleLog.i("BluetoothGattCallback:onServicesDiscovered "
  42. + '\n' + "status: " + status
  43. + '\n' + "currentThread: " + Thread.currentThread().getId());
  44. if (status == BluetoothGatt.GATT_SUCCESS) {
  45. bluetoothGatt = gatt;
  46. connectState = BleConnectState.CONNECT_CONNECTED;
  47. isActiveDisconnect = false;
  48. BleManager.getInstance().getMultipleBluetoothController().addBleBluetooth(BleBluetooth.this);
  49. if (isMainThread) {
  50. Message message = handler.obtainMessage();
  51. message.what = BleMsg.MSG_CONNECT_SUCCESS;
  52. BleConnectStateParameter para = new BleConnectStateParameter(bleGattCallback, gatt, status);
  53. para.setBleDevice(getDevice());
  54. message.obj = para;
  55. handler.sendMessage(message);
  56. } else {
  57. if (bleGattCallback != null)
  58. bleGattCallback.onConnectSuccess(getDevice(), gatt, status);
  59. }
  60. } else {
  61. closeBluetoothGatt();
  62. connectState = BleConnectState.CONNECT_FAILURE;
  63. if (isMainThread) {
  64. Message message = handler.obtainMessage();
  65. message.what = BleMsg.MSG_CONNECT_FAIL;
  66. message.obj = new BleConnectStateParameter(bleGattCallback, gatt, status);
  67. handler.sendMessage(message);
  68. } else {
  69. if (bleGattCallback != null)
  70. bleGattCallback.onConnectFail(new ConnectException(gatt, status));
  71. }
  72. }
  73. }
  74. @Override
  75. public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
  76. super.onCharacteristicChanged(gatt, characteristic);
  77. Iterator iterator = bleNotifyCallbackHashMap.entrySet().iterator();
  78. while (iterator.hasNext()) {
  79. Map.Entry entry = (Map.Entry) iterator.next();
  80. Object callback = entry.getValue();
  81. if (callback instanceof BleNotifyCallback) {
  82. BleNotifyCallback bleNotifyCallback = (BleNotifyCallback) callback;
  83. if (characteristic.getUuid().toString().equalsIgnoreCase(bleNotifyCallback.getKey())) {
  84. Handler handler = bleNotifyCallback.getHandler();
  85. if (handler != null) {
  86. Message message = handler.obtainMessage();
  87. message.what = BleMsg.MSG_CHA_NOTIFY_DATA_CHANGE;
  88. message.obj = bleNotifyCallback;
  89. Bundle bundle = new Bundle();
  90. bundle.putByteArray(BleMsg.KEY_NOTIFY_BUNDLE_VALUE, characteristic.getValue());
  91. message.setData(bundle);
  92. handler.sendMessage(message);
  93. }
  94. }
  95. }
  96. }
  97. iterator = bleIndicateCallbackHashMap.entrySet().iterator();
  98. while (iterator.hasNext()) {
  99. Map.Entry entry = (Map.Entry) iterator.next();
  100. Object callback = entry.getValue();
  101. if (callback instanceof BleIndicateCallback) {
  102. BleIndicateCallback bleIndicateCallback = (BleIndicateCallback) callback;
  103. if (characteristic.getUuid().toString().equalsIgnoreCase(bleIndicateCallback.getKey())) {
  104. Handler handler = bleIndicateCallback.getHandler();
  105. if (handler != null) {
  106. Message message = handler.obtainMessage();
  107. message.what = BleMsg.MSG_CHA_INDICATE_DATA_CHANGE;
  108. message.obj = bleIndicateCallback;
  109. Bundle bundle = new Bundle();
  110. bundle.putByteArray(BleMsg.KEY_INDICATE_BUNDLE_VALUE, characteristic.getValue());
  111. message.setData(bundle);
  112. handler.sendMessage(message);
  113. }
  114. }
  115. }
  116. }
  117. }
  118. @Override
  119. public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
  120. super.onDescriptorWrite(gatt, descriptor, status);
  121. Iterator iterator = bleNotifyCallbackHashMap.entrySet().iterator();
  122. while (iterator.hasNext()) {
  123. Map.Entry entry = (Map.Entry) iterator.next();
  124. Object callback = entry.getValue();
  125. if (callback instanceof BleNotifyCallback) {
  126. BleNotifyCallback bleNotifyCallback = (BleNotifyCallback) callback;
  127. if (descriptor.getCharacteristic().getUuid().toString().equalsIgnoreCase(bleNotifyCallback.getKey())) {
  128. Handler handler = bleNotifyCallback.getHandler();
  129. if (handler != null) {
  130. Message message = handler.obtainMessage();
  131. message.what = BleMsg.MSG_CHA_NOTIFY_RESULT;
  132. message.obj = bleNotifyCallback;
  133. Bundle bundle = new Bundle();
  134. bundle.putInt(BleMsg.KEY_NOTIFY_BUNDLE_STATUS, status);
  135. message.setData(bundle);
  136. handler.sendMessage(message);
  137. }
  138. }
  139. }
  140. }
  141. iterator = bleIndicateCallbackHashMap.entrySet().iterator();
  142. while (iterator.hasNext()) {
  143. Map.Entry entry = (Map.Entry) iterator.next();
  144. Object callback = entry.getValue();
  145. if (callback instanceof BleIndicateCallback) {
  146. BleIndicateCallback bleIndicateCallback = (BleIndicateCallback) callback;
  147. if (descriptor.getCharacteristic().getUuid().toString().equalsIgnoreCase(bleIndicateCallback.getKey())) {
  148. Handler handler = bleIndicateCallback.getHandler();
  149. if (handler != null) {
  150. Message message = handler.obtainMessage();
  151. message.what = BleMsg.MSG_CHA_INDICATE_RESULT;
  152. message.obj = bleIndicateCallback;
  153. Bundle bundle = new Bundle();
  154. bundle.putInt(BleMsg.KEY_INDICATE_BUNDLE_STATUS, status);
  155. message.setData(bundle);
  156. handler.sendMessage(message);
  157. }
  158. }
  159. }
  160. }
  161. }
  162. @Override
  163. public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
  164. super.onCharacteristicWrite(gatt, characteristic, status);
  165. Iterator iterator = bleWriteCallbackHashMap.entrySet().iterator();
  166. while (iterator.hasNext()) {
  167. Map.Entry entry = (Map.Entry) iterator.next();
  168. Object callback = entry.getValue();
  169. if (callback instanceof BleWriteCallback) {
  170. BleWriteCallback bleWriteCallback = (BleWriteCallback) callback;
  171. if (characteristic.getUuid().toString().equalsIgnoreCase(bleWriteCallback.getKey())) {
  172. Handler handler = bleWriteCallback.getHandler();
  173. if (handler != null) {
  174. Message message = handler.obtainMessage();
  175. message.what = BleMsg.MSG_CHA_WRITE_RESULT;
  176. message.obj = bleWriteCallback;
  177. Bundle bundle = new Bundle();
  178. bundle.putInt(BleMsg.KEY_WRITE_BUNDLE_STATUS, status);
  179. bundle.putByteArray(BleMsg.KEY_WRITE_BUNDLE_VALUE, characteristic.getValue());
  180. message.setData(bundle);
  181. handler.sendMessage(message);
  182. }
  183. }
  184. }
  185. }
  186. }
  187. @Override
  188. public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
  189. super.onCharacteristicRead(gatt, characteristic, status);
  190. Iterator iterator = bleReadCallbackHashMap.entrySet().iterator();
  191. while (iterator.hasNext()) {
  192. Map.Entry entry = (Map.Entry) iterator.next();
  193. Object callback = entry.getValue();
  194. if (callback instanceof BleReadCallback) {
  195. BleReadCallback bleReadCallback = (BleReadCallback) callback;
  196. if (characteristic.getUuid().toString().equalsIgnoreCase(bleReadCallback.getKey())) {
  197. Handler handler = bleReadCallback.getHandler();
  198. if (handler != null) {
  199. Message message = handler.obtainMessage();
  200. message.what = BleMsg.MSG_CHA_READ_RESULT;
  201. message.obj = bleReadCallback;
  202. Bundle bundle = new Bundle();
  203. bundle.putInt(BleMsg.KEY_READ_BUNDLE_STATUS, status);
  204. bundle.putByteArray(BleMsg.KEY_READ_BUNDLE_VALUE, characteristic.getValue());
  205. message.setData(bundle);
  206. handler.sendMessage(message);
  207. }
  208. }
  209. }
  210. }
  211. }
  212. };

在收到连接状态、读、写、通知等操作的结果回调之后,通过消息队列机制,交由相应的Handler去处理。那处理消息的Handler在哪里?举例其中的write操作Handler handler = bleWriteCallback.getHandler();,handler对象被包含在了这个write操作的callback中。

  1. public abstract class BleBaseCallback {
  2. private String key;
  3. private Handler handler;
  4. public String getKey() {
  5. return key;
  6. }
  7. public void setKey(String key) {
  8. this.key = key;
  9. }
  10. public Handler getHandler() {
  11. return handler;
  12. }
  13. public void setHandler(Handler handler) {
  14. this.handler = handler;
  15. }
  16. }

所有的操作的callback都继承自这个BleBaseCallback抽象类,它有两个成员变量。一个key,标识着这个callback归属于哪一个Characteristic的操作;另一个handler,用于传递底层发来的操作结果,最终将结果交由callback去抛给调用者,完成一次接口回调。

  1. private void handleCharacteristicWriteCallback(BleWriteCallback bleWriteCallback,
  2. String uuid_write) {
  3. if (bleWriteCallback != null) {
  4. writeMsgInit();
  5. bleWriteCallback.setKey(uuid_write);
  6. bleWriteCallback.setHandler(mHandler);
  7. mBleBluetooth.addWriteCallback(uuid_write, bleWriteCallback);
  8. mHandler.sendMessageDelayed(
  9. mHandler.obtainMessage(BleMsg.MSG_CHA_WRITE_START, bleWriteCallback),
  10. BleManager.getInstance().getOperateTimeout());
  11. }
  12. }

上面这段源码解释了这个机制,每一次write操作之后,都会对传入的callback进行唯一性标记,再通过handler用来传递操作结果,同时将这个callback加入这个设备的BleBlutooth对象的callback池中管理。
这样就形成了APP维持一个设备连接池,一个设备连接池管理多个设备管理者,一个设备管理者管理多个不同类别的callback集合,一个callback集合中含有多个同类的不同特征的callback。
安卓框架整理 - 图3

折线图框架 MPAndroidChart

本文讲述对MPAndroidChart的使用,大部分是从其文档中摘录过来的。版本3.0.2

一、MPAndroidChart

概述
MPAndroidChart 是一款专门用于Android绘制图表的库,相当强大,当然也有IOS的版本。GitHub上地址:Git-MPAndroidChart 。
其特点如下:
支持8中不同图表类型
支持轴上的缩放
可拖动、平移
可定制轴
可突出选中的值(瞄准点及弹出pop)
定制图例
动画显示
其他
下面来详细介绍其使用,涉及到的MPAndroidChart名称都用MP简称来代替。

二、基本使用

1.添加依赖
在project的build.gradle中添加依赖:

  1. allprojects {
  2. repositories {
  3. maven { url https://jitpack.io” }
  4. }
  5. }

在app的build.gradle中添加依赖:

  1. dependencies {
  2. compile com.github.PhilJay:MPAndroidChart:v3.0.2
  3. }

2.布局
在布局文件中使用需要的图表,并在代码中获取实例。

  1. <com.github.mikephil.charting.charts.LineChart
  2. android:id="@+id/chart"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent" />

3.添加数据
首先将数据集合转换成MP需要的数据集合,然后创建DataSet,DataSet保存某个数据集(比如某条曲线),可以定制需要的样式,比如曲线颜色,图例等。最后,添加数据集到LineData中,LineData存放所有的数据,也可以定制样式。

  1. YourData[] dataObjects = ...;
  2. List<Entry> entries = new ArrayList<Entry>();
  3. for (YourData data : dataObjects) {
  4. entries.add(new Entry(data.getValueX(), data.getValueY()));
  5. }
  6. LineDataSet dataSet = new LineDataSet(entries, "Lable");
  7. dataSet.setColor(...);
  8. ...
  9. LineData lineData = new LineData(dataSet);
  10. chart.setData(lineData);
  11. chart.invalidate();


这里要根据不同的表格使用不同的实体类,比如,LineChart使用Entry类,BarChart使用BarEntry类等等。

三、api参考

本来想分模块写的,但是貌似太多了,就放到一个类里了,可能有些用不到,需要时请参考自行定制:

  1. public void showChart() {
  2. // *************************数据转换********************* //
  3. float[] dataObjects = {1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1};
  4. List<Entry> entries = new ArrayList<>();
  5. for (int i = 0; i < dataObjects.length; i++) {
  6. float data = dataObjects[i];
  7. entries.add(new Entry(i, data));
  8. }
  9. LineDataSet dataSet = new LineDataSet(entries, "Label1");
  10. dataSet.setColors(Color.BLACK, Color.GRAY, Color.RED, Color.GREEN); // 每个点之间线的颜色,还有其他几个方法,自己看
  11. dataSet.setValueFormatter(new IValueFormatter() { // 将值转换为想要显示的形式,比如,某点值为1,变为“1¥”,MP提供了三个默认的转换器,
  12. // LargeValueFormatter:将大数字变为带单位数字;PercentFormatter:将值转换为百分数;StackedValueFormatter,对于BarChart,是否只显示最大值图还是都显示
  13. @Override
  14. public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
  15. return value + "¥";
  16. }
  17. });
  18. LineData lineData = new LineData(dataSet);
  19. /*List<ILineDataSet> sets = new ArrayList<>(); // 多条线
  20. sets.add(dataSet);
  21. sets.add(dataSet1);
  22. sets.add(dataSet2);
  23. LineData lineData = new LineData(sets);
  24. */
  25. mLineChart.setData(lineData);
  26. // **************************图表本身一般样式**************************** //
  27. mLineChart.setBackgroundColor(Color.WHITE); // 整个图标的背景色
  28. // mLineChart.setContentDescription("××表"); // 右下角的描述文本,测试并不显示
  29. Description description = new Description(); // 这部分是深度定制描述文本,颜色,字体等
  30. description.setText("××表");
  31. description.setTextColor(Color.RED);
  32. mLineChart.setDescription(description);
  33. mLineChart.setNoDataText("暂无数据"); // 没有数据时样式
  34. mLineChart.setDrawGridBackground(false); // 绘制区域的背景(默认是一个灰色矩形背景)将绘制,默认false
  35. mLineChart.setGridBackgroundColor(Color.WHITE); // 绘制区域的背景色
  36. mLineChart.setDrawBorders(false); // 绘制区域边框绘制,默认false
  37. mLineChart.setBorderColor(Color.GREEN); // 边框颜色
  38. mLineChart.setBorderWidth(2); // 边框宽度,dp
  39. mLineChart.setMaxVisibleValueCount(14); // 数据点上显示的标签,最大数量,默认100。且dataSet.setDrawValues(true);必须为true。只有当数据数量小于该值才会绘制标签
  40. // *********************滑动相关*************************** //
  41. mLineChart.setTouchEnabled(true); // 所有触摸事件,默认true
  42. mLineChart.setDragEnabled(true); // 可拖动,默认true
  43. mLineChart.setScaleEnabled(true); // 两个轴上的缩放,X,Y分别默认为true
  44. mLineChart.setScaleXEnabled(true); // X轴上的缩放,默认true
  45. mLineChart.setScaleYEnabled(true); // Y轴上的缩放,默认true
  46. mLineChart.setPinchZoom(true); // X,Y轴同时缩放,false则X,Y轴单独缩放,默认false
  47. mLineChart.setDoubleTapToZoomEnabled(true); // 双击缩放,默认true
  48. mLineChart.setDragDecelerationEnabled(true); // 抬起手指,继续滑动,默认true
  49. mLineChart.setDragDecelerationFrictionCoef(0.9f); // 摩擦系数,[0-1],较大值速度会缓慢下降,0,立即停止;1,无效值,并转换为0.9999.默认0.9f.
  50. mLineChart.setOnChartGestureListener(new OnChartGestureListener() { // 手势监听器
  51. @Override
  52. public void onChartGestureStart(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {
  53. // 按下
  54. }
  55. @Override
  56. public void onChartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture) {
  57. // 抬起,取消
  58. }
  59. @Override
  60. public void onChartLongPressed(MotionEvent me) {
  61. // 长按
  62. }
  63. @Override
  64. public void onChartDoubleTapped(MotionEvent me) {
  65. // 双击
  66. }
  67. @Override
  68. public void onChartSingleTapped(MotionEvent me) {
  69. // 单击
  70. }
  71. @Override
  72. public void onChartFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) {
  73. // 甩动
  74. }
  75. @Override
  76. public void onChartScale(MotionEvent me, float scaleX, float scaleY) {
  77. // 缩放
  78. }
  79. @Override
  80. public void onChartTranslate(MotionEvent me, float dX, float dY) {
  81. // 移动
  82. }
  83. });
  84. // ************************高亮*************************** //
  85. mLineChart.setHighlightPerDragEnabled(true); // 拖拽时能否高亮(十字瞄准触摸到的点),默认true
  86. mLineChart.setHighlightPerTapEnabled(true); // 双击时能都高亮,默认true
  87. mLineChart.setMaxHighlightDistance(500); // 最大高亮距离(dp),点击位置距离数据点的距离超过这个距离不会高亮,默认500dp
  88. dataSet.setHighlightEnabled(true); // 能否高亮,默认true
  89. dataSet.setDrawHighlightIndicators(true); // 画高亮指示器,默认true
  90. dataSet.setDrawHorizontalHighlightIndicator(true); // 画水平高亮指示器,默认true
  91. dataSet.setDrawVerticalHighlightIndicator(true); // 垂直方向高亮指示器,默认true
  92. dataSet.setHighLightColor(Color.BLACK); // 高亮颜色,默认RGB(255, 187, 115)
  93. mLineChart.highlightValue(1, 0); // 高亮指定值,可以指定数据集的值,还有其他几个重载方法
  94. mLineChart.setOnChartValueSelectedListener(new OnChartValueSelectedListener() { // 值选择监听器
  95. @Override
  96. public void onValueSelected(Entry e, Highlight h) {
  97. // 选中
  98. }
  99. @Override
  100. public void onNothingSelected() {
  101. // 未选中
  102. }
  103. });
  104. // *************************轴****************************** //
  105. // 由四个元素组成:
  106. // 标签:即刻度值。也可以自定义,比如时间,距离等等,下面会说一下;
  107. // 轴线:坐标轴;
  108. // 网格线:垂直于轴线对应每个值画的轴线;
  109. // 限制线:最值等线。
  110. XAxis xAxis = mLineChart.getXAxis(); // 获取X轴
  111. YAxis yAxis = mLineChart.getAxisLeft(); // 获取Y轴,mLineChart.getAxis(YAxis.AxisDependency.LEFT);也可以获取Y轴
  112. mLineChart.getAxisRight().setEnabled(false); // 不绘制右侧的轴线
  113. xAxis.setEnabled(true); // 轴线是否可编辑,默认true
  114. xAxis.setDrawLabels(true); // 是否绘制标签,默认true
  115. xAxis.setDrawAxisLine(true); // 是否绘制坐标轴,默认true
  116. xAxis.setDrawGridLines(false); // 是否绘制网格线,默认true
  117. xAxis.setAxisMaximum(10); // 此轴能显示的最大值;
  118. xAxis.resetAxisMaximum(); // 撤销最大值;
  119. xAxis.setAxisMinimum(1); // 此轴显示的最小值;
  120. xAxis.resetAxisMinimum(); // 撤销最小值;
  121. // yAxis.setStartAtZero(true); // 从0开始绘制。已弃用。使用setAxisMinimum(float);
  122. // yAxis.setInverted(true); // 反转轴,默认false
  123. yAxis.setSpaceTop(10); // 设置最大值到图标顶部的距离占所有数据范围的比例。默认10,y轴独有
  124. // 算法:比例 = (y轴最大值 - 数据最大值)/ (数据最大值 - 数据最小值) ;
  125. // 用途:可以通过设置该比例,使线最大最小值不会触碰到图标的边界。
  126. // 注意:设置一条线可能不太好看,mLineChart.getAxisRight().setSpaceTop(34)也设置比较好;同时,如果设置最小值,最大值,会影响该效果
  127. yAxis.setSpaceBottom(10); // 同上,只不过是最小值距离底部比例。默认10,y轴独有
  128. // yAxis.setShowOnlyMinMax(true); // 没找到。。。,true, 轴上只显示最大最小标签忽略指定的数量(setLabelCount,如果forced = false).
  129. yAxis.setLabelCount(4, false); // 纵轴上标签显示的数量,数字不固定。如果force = true,将会画出明确数量,但是可能值导致不均匀,默认(6,false)
  130. yAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART); // 标签绘制位置。默认再坐标轴外面
  131. xAxis.setGranularity(1); // 设置X轴值之间最小距离。正常放大到一定地步,标签变为小数值,到一定地步,相邻标签都是一样的。这里是指定相邻标签间最小差,防止重复。
  132. yAxis.setGranularity(1); // 同上
  133. yAxis.setGranularityEnabled(false); // 是否禁用上面颗粒度限制。默认false
  134. // 轴颜色
  135. yAxis.setTextColor(Color.RED); // 标签字体颜色
  136. yAxis.setTextSize(10); // 标签字体大小,dp,6-24之间,默认为10dp
  137. yAxis.setTypeface(null); // 标签字体
  138. yAxis.setGridColor(Color.GRAY); // 网格线颜色,默认GRAY
  139. yAxis.setGridLineWidth(1); // 网格线宽度,dp,默认1dp
  140. yAxis.setAxisLineColor(Color.RED); // 坐标轴颜色,默认GRAY.测试到一个bug,假如左侧线只有1dp,
  141. // 那么如果x轴有线且有网格线,当刻度拉的正好位置时会覆盖到y轴的轴线,变为X轴网格线颜色,结局办法是,要么不画轴线,要么就是坐标轴稍微宽点
  142. xAxis.setAxisLineColor(Color.RED);
  143. yAxis.setAxisLineWidth(2); // 坐标轴线宽度,dp,默认1dp
  144. yAxis.enableGridDashedLine(20, 10, 1); // 网格线为虚线,lineLength,每段实线长度,spaceLength,虚线间隔长度,phase,起始点(进过测试,最后这个参数也没看出来干啥的)
  145. // 限制线
  146. LimitLine ll = new LimitLine(6.5f, "上限"); // 创建限制线, 这个线还有一些相关的绘制属性方法,自行看一下就行,没多少东西。
  147. yAxis.addLimitLine(ll); // 添加限制线到轴上
  148. yAxis.removeLimitLine(ll); // 移除指定的限制线,还有其他的几个移除方法
  149. yAxis.setDrawLimitLinesBehindData(false); // 限制线在数据之后绘制。默认为false
  150. // X轴更多属性
  151. xAxis.setLabelRotationAngle(45); // 标签倾斜
  152. xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); // X轴绘制位置,默认是顶部
  153. // Y轴更多属性
  154. dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); // 设置dataSet应绘制在Y轴的左轴还是右轴,默认LEFT
  155. yAxis.setDrawZeroLine(false); // 绘制值为0的轴,默认false,其实比较有用的就是在柱形图,当有负数时,显示在0轴以下,其他的图这个可能会看到一些奇葩的效果
  156. yAxis.setZeroLineWidth(10); // 0轴宽度
  157. yAxis.setZeroLineColor(Color.BLUE); // 0轴颜色
  158. // 轴值转换显示
  159. xAxis.setValueFormatter(new IAxisValueFormatter() { // 与上面值转换一样,这里就是转换出轴上label的显示。也有几个默认的,不多说了。
  160. @Override
  161. public String getFormattedValue(float value, AxisBase axis) {
  162. return value + "℃";
  163. }
  164. });
  165. // *********************图例***************************** //
  166. Legend legend = mLineChart.getLegend(); // 获取图例,但是在数据设置给chart之前是不可获取的
  167. legend.setEnabled(true); // 是否绘制图例
  168. legend.setTextColor(Color.GRAY); // 图例标签字体颜色,默认BLACK
  169. legend.setTextSize(12); // 图例标签字体大小[6,24]dp,默认10dp
  170. legend.setTypeface(null); // 图例标签字体
  171. legend.setWordWrapEnabled(false); // 当图例超出时是否换行适配,这个配置会降低性能,且只有图例在底部时才可以适配。默认false
  172. legend.setMaxSizePercent(1f); // 设置,默认0.95f,图例最大尺寸区域占图表区域之外的比例
  173. legend.setPosition(Legend.LegendPosition.BELOW_CHART_LEFT); // 图例显示位置,已弃用
  174. legend.setForm(Legend.LegendForm.CIRCLE); // 设置图例的形状,SQUARE, CIRCLE 或者 LINE
  175. legend.setFormSize(8); // 图例图形尺寸,dp,默认8dp
  176. legend.setXEntrySpace(6); // 设置水平图例间间距,默认6dp
  177. legend.setYEntrySpace(0); // 设置垂直图例间间距,默认0
  178. legend.setFormToTextSpace(5); // 设置图例的标签与图形之间的距离,默认5dp
  179. legend.setWordWrapEnabled(true); // 图标单词是否适配。只有在底部才会有效,
  180. legend.setCustom(new LegendEntry[]{new LegendEntry("label1", Legend.LegendForm.CIRCLE, 10, 5, null, Color.RED),
  181. new LegendEntry("label2", Legend.LegendForm.CIRCLE, 10, 5, null, Color.GRAY),
  182. new LegendEntry("label3", Legend.LegendForm.CIRCLE, 10, 5, null, Color.RED)}); // 这个应该是之前的setCustom(int[] colors, String[] labels)方法
  183. // 这个方法会把前面设置的图例都去掉,重置为指定的图例。
  184. legend.resetCustom(); // 去掉上面方法设置的图例,然后之前dataSet中设置的会重新显示。
  185. // legend.setExtra(new int[]{Color.RED, Color.GRAY, Color.GREEN}, new String[]{"label1", "label2", "label3"}); // 添加图例,颜色与label数量要一致。
  186. // 如果前面已经在dataSet中设置了颜色,那么之前的图例就存在,这个只是添加在后面的图例,并不一定有对应数据。
  187. mLineChart.invalidate(); // 重绘
  188. // ********************其他******************************* //
  189. mLineChart.setLogEnabled(false); // 是否打印日志,默认false
  190. // mLineChart.notifyDataSetChanged(); // 通知有值变化,重绘,一般动态添加数据时用到
  191. // ******************指定缩放显示范围************************* //
  192. // 这里要说一下,下面并不是指定其初始显示的范围,所以,很可能大家觉得没有效果。其实这几个方法目的是限制缩放时的可见范围最值。
  193. // mLineChart.setVisibleXRangeMaximum(6); // X轴缩小可见最大范围,这里测试有点问题,范围不是指定的,可以缩小到更多范围。
  194. // mLineChart.setVisibleXRangeMinimum(4); // X轴放大最低可见范围,最小意思是,再怎么放大范围也至少要有4,但是一开始显示的时候范围可能很大。
  195. // mLineChart.setVisibleYRangeMaximum(4, YAxis.AxisDependency.LEFT); // Y缩小时可见最大范围,后面是其适用的轴。测试发现两边轴都是有效的
  196. // mLineChart.setVisibleYRangeMinimum(2, YAxis.AxisDependency.LEFT); // Y轴放大时可见最小范围。
  197. // mLineChart.setVisibleYRange(3, 5, YAxis.AxisDependency.LEFT); // y轴缩放时可见最小和最大范围。但是测试发现不能放大3的范围,但是也是符合这个限制的
  198. // mLineChart.setVisibleXRange(3, 6); // X轴缩放时可见最小和最大范围。测试也有点问题
  199. // mLineChart.setViewPortOffsets(10, 0, 10, 0); // 图表绘制区的偏移量设置,这个会忽略MP的自动计算偏移。
  200. // 比如,自动时,图例与绘制区是分开的,但是自己写就可能重和在一起。慎用
  201. // mLineChart.resetViewPortOffsets(); // 重置上面的偏移量设置。
  202. // mLineChart.setExtraOffsets(10, 0, 10, 0); // 这个与上面的区别是不会忽略其自己计算的偏移。
  203. // **************************移动******************************** //
  204. // mLineChart.fitScreen(); // 重置所有缩放与拖动,使图标完全符合其边界
  205. // mLineChart.moveViewToX(30); // 想指定向偏移,比如原本显示前三个点,现在显示后三个,如果没有缩放其实看不出啥效果
  206. // mLineChart.moveViewTo(30, 10, YAxis.AxisDependency.LEFT); // 向指定方向偏移,如果没有缩放其实看不出啥效果,后面的轴没啥效果
  207. // mLineChart.moveViewToAnimated(30, 10, YAxis.AxisDependency.LEFT, 2000); // 同上面那个,但是有动画效果
  208. // mLineChart.centerViewTo(30, 10, YAxis.AxisDependency.LEFT); // 将视图中心移动到指定位置,也是要缩放才有效果
  209. // mLineChart.centerViewToAnimated(30, 10, YAxis.AxisDependency.LEFT, 2000); // 同上面那个,但是有动画效果
  210. // ****************************自动缩放********************************** //
  211. // 这里的缩放效果会收到setVisibleXRangeMaximum等范围影响,
  212. // mLineChart.zoomIn(); // 自动放大1.4倍,没看出效果
  213. // mLineChart.zoomOut(); // 自动缩小0.7倍,没看出效果
  214. // mLineChart.zoom(2f, 2f, 2, 3, YAxis.AxisDependency.LEFT);
  215. // mLineChart.zoomAndCenterAnimated(1.4f, 1.4f, 2, 3, YAxis.AxisDependency.LEFT, 3000); // 缩放,有动画,报了个空指针。。。
  216. // ************************动画************************************** //
  217. /*mLineChart.animateX(3000); // 数据从左到右动画依次显示
  218. mLineChart.animateY(3000); // 数据从下到上动画依次显示*/
  219. // mLineChart.animateXY(3000, 3000); // 上面两个的结合
  220. mLineChart.animateX(3000, Easing.EasingOption.EaseInQuad); // 动画播放随时间变化的速率,有点像插值器。后面这个有的不能用
  221. // **************************所有数据样式************************************ //
  222. mLineChart.setMarker(new ChartMarkerView(this, R.layout.item_chart_indicator, "温度:", "℃")); // 点击数据点显示的pop,有俩默认的,MarkerImage:一张图片,MarkerView:一个layout布局,也可以自己定义.这里这个是我自定义的一个MarkerView。
  223. lineData.setValueTextColor(Color.RED); // 该条线的
  224. List<Integer> colors = new ArrayList<>();
  225. colors.add(Color.BLACK);
  226. colors.add(Color.GRAY);
  227. colors.add(Color.RED);
  228. colors.add(Color.GREEN);
  229. lineData.setValueTextColors(colors); // 字体添加颜色,按顺序给数据上色,不足则重复使用,也可以在单个dataSet上添加
  230. lineData.setValueTextSize(12); // 文字大小
  231. lineData.setValueTypeface(null); // 文字字体
  232. lineData.setValueFormatter(new IValueFormatter() { // 所有数据显示的数据值
  233. @Override
  234. public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
  235. return value + "¥";
  236. }
  237. });
  238. lineData.setDrawValues(true); // 绘制每个点的值
  239. // 上面这些都是data集合中的相关属性,也可以针对每个dataSet来设置
  240. // **************************图表本身特殊样式******************************** //
  241. mLineChart.setAutoScaleMinMaxEnabled(false); // y轴是否自动缩放;当缩放时,y轴的显示会自动根据x轴范围内数据的最大最小值而调整。财务报表比较有用,默认false
  242. mLineChart.setKeepPositionOnRotation(false); // 设置当屏幕方向变化时,是否保留之前的缩放与滚动位置,默认:false
  243. // *****************************其他的chart************************* //
  244. // 下面只有barChart(柱状图)有用
  245. BarChart mBarChart = (BarChart) findViewById(R.id.bc);
  246. List<BarEntry> barEntries = new ArrayList<>();
  247. barEntries.add(new BarEntry(0, 1));
  248. barEntries.add(new BarEntry(1, 2));
  249. barEntries.add(new BarEntry(2, 3));
  250. barEntries.add(new BarEntry(3, -1));
  251. BarDataSet iBarDataSet = new BarDataSet(barEntries, "bar label");
  252. iBarDataSet.setColors(colors);
  253. iBarDataSet.setValueTextColors(colors);
  254. BarData barData = new BarData(iBarDataSet); // 可以添加多个set,即可化成group组
  255. mBarChart.setData(barData);
  256. // mBarChart.groupBars(1980f, 20, 0); // 设置group组间隔
  257. mBarChart.setFitBars(true); // 在bar开头结尾两边添加一般bar宽的留白
  258. mBarChart.setDrawValueAboveBar(false); // 所有值都绘制在柱形外顶部,而不是柱形内顶部。默认true
  259. mBarChart.setDrawBarShadow(false); // 柱形阴影,一般有值被绘制,但是值到顶部的位置为空,这个方法设置也画这部分,但是性能下降约40%,默认false
  260. // setDrawValuesForWholeStack(boolean enabled); // 没有该方法。。。是否绘制堆积的每个值,还是只是画堆积的总值,
  261. // setDrawHighlightArrow(true); // 没有该方法。。。是否绘制高亮箭头
  262. // 下面只有PieChart(饼状图)有用
  263. PieChart mPieChart = (PieChart) findViewById(R.id.pc);
  264. List<PieEntry> pieEntries = new ArrayList<>();
  265. pieEntries.add(new PieEntry(1, "11"));
  266. pieEntries.add(new PieEntry(2, "22"));
  267. pieEntries.add(new PieEntry(3, "33"));
  268. PieDataSet iPieDataSet = new PieDataSet(pieEntries, "pie label");
  269. iPieDataSet.setColors(colors);
  270. iPieDataSet.setValueTextColors(colors);
  271. iPieDataSet.setSliceSpace(3); // 每块之间的距离
  272. PieData pieData = new PieData(iPieDataSet);
  273. mPieChart.setData(pieData);
  274. /*mPieChart.setDrawSliceText(true);*/ // : 将X值绘制到饼状图环切片内,否则不显示。默认true,已弃用,用下面setDrawEntryLabels()
  275. mPieChart.setDrawEntryLabels(true); // 同上,默认true,记住颜色和环不要一样,否则会显示不出来
  276. mPieChart.setUsePercentValues(true); // 表内数据用百分比替代,而不是原先的值。并且ValueFormatter中提供的值也是该百分比的。默认false
  277. mPieChart.setCenterText("asc"); // 圆环中心的文字,会自动适配不会被覆盖
  278. mPieChart.setCenterTextRadiusPercent(100f); // 中心文本边界框矩形半径比例,默认是100%.
  279. mPieChart.setHoleRadius(60); // 设置中心圆半径占整个饼形图圆半径(图表半径)的百分比。默认50%
  280. mPieChart.setTransparentCircleRadius(70); // 设置环形与中心圆之间的透明圆环半径占图表半径的百分比。默认55%(比如,中心圆为50%占比,而透明环设置为55%占比,要去掉中心圆的占比,也就是环只有5%的占比)
  281. mPieChart.setTransparentCircleColor(Color.RED); // 上述透明圆环的颜色
  282. mPieChart.setTransparentCircleAlpha(50); // 上述透明圆环的透明度[0-255],默认100
  283. mPieChart.setMaxAngle(360); // 设置整个饼形图的角度,默认是360°即一个整圆,也可以设置为弧,这样现实的值也会重新计算
  284. // 下面只有RadarChart(雷达图)有用
  285. RadarChart mRadarChart = (RadarChart) findViewById(R.id.rc);
  286. List<RadarEntry> radarEntries = new ArrayList<>();
  287. radarEntries.add(new RadarEntry(1, "111"));
  288. radarEntries.add(new RadarEntry(2, "222"));
  289. radarEntries.add(new RadarEntry(3, "333"));
  290. radarEntries.add(new RadarEntry(4, "444"));
  291. radarEntries.add(new RadarEntry(5, "555"));
  292. RadarDataSet iRadarDataSet = new RadarDataSet(radarEntries, "bar label");
  293. iRadarDataSet.setColors(colors);
  294. iRadarDataSet.setValueTextColors(colors);
  295. RadarData radarData = new RadarData(iRadarDataSet);
  296. mRadarChart.setData(radarData);
  297. mRadarChart.setSkipWebLineCount(8); // 允许不绘制从中心发出的线,当线多时较有用。默认为0
  298. // *************************其他********************************* //
  299. // 上面介绍了MP的大部分常用的api,基本可以满足绝大部分的需求,还有部分不常用的暂时不说了,以后用的着再下面补充
  300. // mLineChart.clear(); // 清空
  301. }

废话也不多说了,上述所有接口都是测试过的,有些我标明没效果或者异常的,可能是我方法不对,使用时请自行测试。当然可能用不着那么多,使用时留下需要的即可。
来两张效果图:
安卓框架整理 - 图4

安卓框架整理 - 图5

四、常见异常及处理

这块是我后面补上去的,因为最近项目里出现了几个异常,让我很蛋疼。下面的几个问题的原因分析是我推测的,并没有直接看源码,八九不离十,如果有看了源码知道确切原因请指出,我会改正。
IndexOutOfBoundsException
首先是看图:
安卓框架整理 - 图6
额,数组越界。那么哪里越界呢,从异常里是看不出的,其实就是LineDataSet对象数据为空,在绘制的时候,没有做判断就获取数据,结果没有数据时,直接gg。
解决:添加数据时判断是否为空,空了就不添加。
创建负数数组
安卓框架整理 - 图7
这个是创建了负数数组的问题,原因在于添加数据的顺序有误。就是每个LineDataSet的数据Entry的下标x要从小到大,不能倒序。
解决:要注意给LineDataSet添加数据时,每个Entry的x要从小到大来排,不能随意添加或者倒序。
其实这个问题大部分人可能遇不到,问题在哪呢。我项目中遇到的是这样的,后台返回了个倒序数据集合,而且数据有x值(int)。前人就没当回事(我也是接了坑了,当然我确实不知道MP有这个问题),直接往LineDataSet添加,结果第一个下标为15,最后一个为0,这样就死得难看了。但是我们一般不会直接用给定的值,因为一般不会是数字型的数据,有的还要转换,需要另外存储,下标肯定是从0开始依次增加的。

五、标签格式化与标记定制

1.IValueFormatter
数据点的值标签转换

  1. public class MyValueFormatter implements IValueFormatter {
  2. public MyValueFormatter() {
  3. // 可以在这里执行一些用到的初始化操作,比如DecimalFormat初始化
  4. }
  5. @Override
  6. public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
  7. // value是当前数据点的值,可以将其转换为带单位或其他内容。
  8. return value + "¥";
  9. }
  10. }

2.IAxisValueFormatter
坐标轴的刻度标签转换

  1. public class MyYAxisValueFormatter implements IAxisValueFormatter {
  2. public MyAxisValueFormatter() {
  3. // 初始化操作
  4. }
  5. @Override
  6. public String getFormattedValue(float value, AxisBase axis) {
  7. return value + "d";
  8. }
  9. }


这个在X轴或Y轴都可以设置,应用中可以只传递数值,然后再用formatter来转添加单位。
3.IMarker
当点击选中数据时,提示的pop

  1. public class YourMarkerView extends MarkerView {
  2. private TextView tvContent;
  3. public MyMarkerView(Context context, int layoutResource) {
  4. super(context, layoutResource);
  5. // find your layout components
  6. tvContent = (TextView) findViewById(R.id.tvContent);
  7. }
  8. // 每次MarKerView重绘时调用,可以用于更新内容
  9. @Override
  10. public void refreshContent(Entry e, Highlight highlight) {
  11. tvContent.setText("" + e.getY());
  12. // 执行必要的布局
  13. super.refreshContent(e, highlight);
  14. }
  15. private MPPointF mOffset;
  16. @Override
  17. public MPPointF getOffset() {
  18. if(mOffset == null) {
  19. // center the marker horizontally and vertically
  20. mOffset = new MPPointF(-(getWidth() / 2), -getHeight());
  21. }
  22. return mOffset;
  23. }
  24. }

这个是继承自MarkerView,一种方便的实现方式,通常继承这个就够了,如果想高度定制,可以实现IMarker接口。
给出我上述自定义的ChartMarkerView:

  1. package com.david.study.company;
  2. import android.content.Context;
  3. import android.widget.TextView;
  4. import com.david.study.R;
  5. import com.github.mikephil.charting.components.MarkerView;
  6. import com.github.mikephil.charting.data.Entry;
  7. import com.github.mikephil.charting.highlight.Highlight;
  8. import com.github.mikephil.charting.utils.MPPointF;
  9. /**
  10. * 测试markerView
  11. * Created by DavidChen on 2017/7/13.
  12. */
  13. public class ChartMarkerView extends MarkerView {
  14. TextView tv_indicator;
  15. String item;
  16. String unit;
  17. /**
  18. * Constructor. Sets up the MarkerView with a custom layout resource.
  19. *
  20. * @param layoutResource the layout resource to use for the MarkerView
  21. */
  22. public ChartMarkerView(Context context, int layoutResource, String item, String unit) {
  23. super(context, layoutResource);
  24. tv_indicator = (TextView) findViewById(R.id.tv_indicator);
  25. this.item = null == item ? "" : item;
  26. this.unit = null == unit ? "" : unit;
  27. }
  28. private boolean isReverse = true;
  29. @Override
  30. public void refreshContent(Entry e, Highlight highlight) {
  31. isReverse = !(highlight.getX() > 8);
  32. String content = "时间:";
  33. content += e.getX() + "d";
  34. content += "\n" + item + e.getY() + unit;
  35. tv_indicator.setText(content);
  36. super.refreshContent(e, highlight);
  37. }
  38. @Override
  39. public MPPointF getOffset() {
  40. MPPointF mpPointF = super.getOffset();
  41. if (!isReverse ) {
  42. mpPointF.x = -tv_indicator.getWidth();
  43. } else {
  44. mpPointF.x = 0;
  45. }
  46. mpPointF.y = -tv_indicator.getHeight();
  47. return mpPointF;
  48. }
  49. }

这里可能在显示边上的点时位置还欠考虑。仅供参考。