分类 Android开发 下的文章

1. 悬浮窗口的实现

使用悬浮窗口之前,要向用户申请权限

//在清单文件中定义权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

目标版本安卓6.0以上,还要在运行时申请权限

//在运行时申请权限
if (!Settings.canDrawOverlays(this)){
    Intent floatPermission = new Intent(
        Settings.ACTION_MANAGE_OVERLAY_PERMISSION, //不同于一般的权限,悬浮窗口权限是在独立的界面请求的
        Uri.parse("package:" + getPackageName())); //带上自己的包名,打开自己软件的权限设置界面
    startActivityForResult(floatPermission,REQ_FLOAT_PERMISSION); //打开设置界面
}

悬浮窗口的实现原理是利用WindowManager直接向屏幕添加视图

//获取WindowManager
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//适应安卓API的变化
int DEFAULT_WINDOW_TYPE;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; //最新安卓版本将应用的悬浮窗口独立出来了,使用专用的窗口类型
}else{
    DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_PHONE; //早期安卓版本,悬浮窗口使用PHONE窗口类型
}
//定义视图属性
lp = new WindowManager.LayoutParams();
lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL //悬浮窗口不会占满屏幕,所以设置为视图外部不拦截点击事件
        |WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN //限制窗口显示在屏幕之类,按需设置
        |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //禁止悬浮窗口获取输入焦点,否则返回键之类的按键事件在别的应用程序中会失效,而且别的应用也不能输入文字
lp.format = PixelFormat.RGBA_8888; //设置颜色格式,支持透明
lp.gravity = Gravity.LEFT|Gravity.TOP; //重力左上角,方便调整位置和大小
lp.width = 320; //窗口宽320个像素
lp.height = 320; //窗口高320个像素
lp.x = 0; //距离屏幕左边0像素
lp.y = 0; //距离屏幕上方0像素
//这里的view可以是任意View类对象
//将视图属性赋给视图对象,并添加到屏幕上去
wm.addView(view, lp);
//使用结束,将视图从屏幕上移除
wm.removeView(view);

这里为了适应新版安卓,早期安卓使用TYPE_PHONE作为窗口类型,较新的安卓使用TYPE_APPLICATION_OVERLAY作为窗口类型

2. 用户交互事件监听

像正常的视图对象一样,调用setOnClickListener之类的方法即可实现点击事件的监听

如果要监听窗口拖动的事件,最好在界面上布置一个透明Button,在其上面监听touch事件。实测的时候,直接在LinearLayout或TextView对象上面监听touch事件,只能收到touch_down回调,touch_move和touch_up事件都收不到。

3. 封装及使用

先来看看封装过后绘制出来的悬浮窗口长啥样:3
显示悬浮窗口
屏幕截图.png
调整大小事件
屏幕截图.png
拖动事件
屏幕截图.png
以下是封装的代码


import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;


/*
 * Manage the float windows easily
 * Make the window looks like the window on Windows  :p
 * Created by Dreagonmon on 2017/1/15.
 * 记得先申请悬浮窗权限!如果只想用APPLICATION层面的窗口,请修改DEFAULT_WINDOW_TYPE
 */

public class FloatWindow
{
    public final int DEFAULT_WINDOW_TYPE;
    public final int scrWidth;
    public final int scrHeight;
    public int minWidth;
    public int maxWidth;
    public int minHeight;
    public int maxHeight;
    private final WindowManager wm;
    private final Context context;
    private boolean isShowing = false;
    private View view;//内容对象
    private View window;//窗口对象
    private WindowManager.LayoutParams lp;
    private TouchListenerMove listenerMove = new TouchListenerMove();
    private TouchListenerResize listenerResize = new TouchListenerResize();
    private Runnable onPressAction;//点击事件
    public FloatWindow(Context context)
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        }else{
            DEFAULT_WINDOW_TYPE = WindowManager.LayoutParams.TYPE_PHONE;
        }
        this.wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        this.context = context;
        DisplayMetrics DM = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(DM);
        maxWidth = scrWidth = DM.widthPixels;
        maxHeight = scrHeight = DM.heightPixels;
        minWidth = 0;
        minHeight = 0;
        readyLp();
    }

    public FloatWindow(Context context,View view)
    {
        this(context);
        this.setView(view);
    }
    private void readyLp()
    {
        lp = new WindowManager.LayoutParams();
        lp.type = DEFAULT_WINDOW_TYPE;//悬浮窗口类型,记得给予权限
        //背景可点击,保持在屏幕内
        lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                |WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        lp.format = PixelFormat.RGBA_8888;//透明背景
        lp.gravity = Gravity.LEFT|Gravity.TOP;//重力左上角,方便调整大小
        lp.width = scrWidth;
        lp.height = scrHeight;
        lp.x = 0;
        lp.y = 0;
    }


    /*内部元素操作类方法*/
    /*
    * setWindow()与setView()的区别:
    * 一个会自动包裹进窗口内部,一个就是单纯的传入视图
    * 当使用了setView的时候,getWindow()与getView()效果一样,
    * 当使用了setWindow的时候,getWindow()返回窗口整体,getView()返回之前包裹的视图,*/
    public View setWindow(int styleLayoutID,int containerID, View v)
    {
        /*主要内容容器
        * 自定义layout布局,为styleLayoutID所指定的布局
        * 最后自由添加内容的FrameLayout容器的ID为window_container*/
        if (window == null||window.getId()!=styleLayoutID)
        {
            window = LayoutInflater.from(context).inflate(styleLayoutID,null);
        }
        FrameLayout container = (FrameLayout) window.findViewById(containerID);
        v.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.MATCH_PARENT));
        container.removeAllViews();
        container.addView(v);
        this.view = v;
        return window;
    }
    public View getWindow()
    {
        return window;
    }
    public void setView(View view)
    {
        this.view = window = view;
    }
    public View getView()
    {
        return view;
    }
    public View getElementById(int ID)
    {
        return window.findViewById(ID);
    }
    public WindowManager.LayoutParams getLayoutParams()
    {
        return lp;
    }
    public void updateWindow()//配合取出的LayoutParams使用,记得改过lp之后更新窗口!
    {
        if (isShowing)
        {
            wm.updateViewLayout(window,lp);
        }
    }
    public boolean isShowing()
    {
        return isShowing;
    }


    /*窗口设置类方法*/
    /*
    * 这几个事件最好是由窗口样式中的元素来设置,这样在更改内容View之后不需要再次设置
    * OnPressAction仅作用于设置的MoveView,拖动距离不远的话算作点击,
    * 该方法的调用在主线程,不要放置网络连接等操作,如果想取消点击事件,传入null即可
    * Move和Resize被限制在屏幕之内,这是为了保证窗口的稳定
    * 移动和改变大小事件推荐设置在Button上,TextView对touch事件的触发不完整
    * 只有touch_down事件触发了回调*/
    public void setMoveView(View v)
    {
        v.setOnTouchListener(listenerMove);
    }
    public void setMoveView(int ID)
    {
        View v = window.findViewById(ID);
        if(v != null)
        {
            v.setOnTouchListener(listenerMove);
        }
    }
    public void setResizeView(View v)
    {
        v.setOnTouchListener(listenerResize);
    }
    public void setResizeView(int ID)
    {
        View v = window.findViewById(ID);
        if(v != null)
        {
            v.setOnTouchListener(listenerResize);
        }
    }
    public void setOnPressAction(Runnable run)
    {
        onPressAction = run;
    }


    /*窗口行为类方法*/
    /*记得先resize再move
    * 为了保证所有按钮可用,这里的移动和调整大小的事件被限制为在屏幕之内*/
    public void moveTo(int x,int y)
    {
        lp.x = x<0?0:(x+lp.width<=scrWidth?x:scrWidth-lp.width);
        lp.y = y<0?0:(y+lp.height<=scrHeight?y:scrHeight-lp.height);
        if (isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void moveBy(float xOffset,float yOffset)
    {
        lp.x =(int) (lp.x+xOffset<0?0:(lp.x+lp.width+xOffset<=scrWidth?lp.x+xOffset:scrWidth-lp.width));
        lp.y =(int) (lp.y+yOffset<0?0:(lp.y+lp.height+yOffset<=scrHeight?lp.y+yOffset:scrHeight-lp.height));
        if (isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void resizeTo(int width,int height)
    {
        lp.width = width<minWidth?minWidth:(width<=maxWidth-16?width:maxWidth);
        lp.height = height<minHeight?minHeight:(height<=maxHeight-16?height:maxHeight);
        if(isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void resizeBy(float xOffset,float yOffset)
    {
        lp.width =(int) (lp.width+xOffset<minWidth?minWidth:(lp.width+xOffset<=maxWidth?lp.width+xOffset:maxWidth));
        lp.height =(int) (lp.height+yOffset<minHeight?minHeight:(lp.height+yOffset<=maxHeight?lp.height+yOffset:maxHeight));
        if (isShowing)
        wm.updateViewLayout(window,lp);
    }
    public void show()
    {
        if (!isShowing)
        {
            isShowing = true;
            wm.addView(window, lp);
        }
    }
    public void hide()
    {
        if (isShowing)
        {
            isShowing = false;
            wm.removeView(window);
        }
    }

    /*设计或许会用到的dp转px*/
    public static int dp2px(Context context, float dipValue)
    {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int)(dipValue * scale + 0.5f);
    }

    /*默认执行了移动和改变大小的点击事件后会移到最上层*/
    class TouchListenerMove implements View.OnTouchListener
    {
        float lX,lY;
        int startX,startY;
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    startX = lp.x;
                    startY = lp.y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    moveBy(motionEvent.getRawX()-lX,motionEvent.getRawY()-lY);
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    break;
                case MotionEvent.ACTION_UP:
                    lX = lY = 0;
                    if (Math.abs(lp.x-startX)<5&&Math.abs(lp.y-startY)<5)
                    {
                        moveTo(startX,startY);
                        if (onPressAction!=null)
                        {
                            onPressAction.run();
                        }
                    }
                    //hide();
                    //show();
                    break;
            }
            return false;
        }
    }
    class TouchListenerResize implements View.OnTouchListener
    {
        View tmpView;
        WindowManager.LayoutParams tmpLp = new WindowManager.LayoutParams();
        float lX,lY;
        private void resizeTmpView(float xOffset,float yOffset)
        {
            tmpLp.width =(int) (tmpLp.width+xOffset<minWidth?minWidth:(tmpLp.width+xOffset<=maxWidth?tmpLp.width+xOffset:maxWidth));
            tmpLp.height =(int) (tmpLp.height+yOffset<minHeight?minHeight:(tmpLp.height+yOffset<=maxHeight?tmpLp.height+yOffset:maxHeight));
            if (isShowing)
                wm.updateViewLayout(tmpView,tmpLp);
        }
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            switch (motionEvent.getAction())
            {
                case MotionEvent.ACTION_DOWN:
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    if (tmpView==null)
                    {
                        tmpView = new View(context);
                        tmpView.setBackgroundColor(Color.parseColor("#800080FF"));
                    }
                    tmpLp.x = lp.x;
                    tmpLp.y = lp.y;
                    tmpLp.width = lp.width;
                    tmpLp.height = lp.height;
                    tmpLp.type = lp.type;
                    tmpLp.flags = lp.flags;
                    tmpLp.format = lp.format;
                    tmpLp.gravity = lp.gravity;
                    wm.addView(tmpView,tmpLp);
                    break;
                case MotionEvent.ACTION_MOVE:
                    resizeTmpView(motionEvent.getRawX()-lX,motionEvent.getRawY()-lY);
                    lX = motionEvent.getRawX();
                    lY = motionEvent.getRawY();
                    break;
                case MotionEvent.ACTION_UP:
                    lX = lY = 0;
                    resizeTo(tmpLp.width,tmpLp.height);
                    wm.removeView(tmpView);
                    //hide();
                    //show();
                    break;
            }
            return false;
        }
    }
}

以下是使用的代码

FloatWindow fw = new FloatWindow(this);
fw.setWindow(R.layout.float_window_frame,R.id.window_container,new TextView(this));
fw.setMoveView(R.id.window_move);
fw.setResizeView(R.id.window_resize);
fw.minWidth = fw.minHeight = 320;
fw.resizeTo(480,640);
fw.moveTo(240,320);
fw.show();


低功耗蓝牙 BLE 通讯篇

继续研究安卓上面蓝牙BLE编程,一部安卓设备模拟服务端,一部安卓手机模拟客户端,看着官方文档摸索如何用BLE在两部安卓设备之间交换数据。

0.客户端与服务端连接

在客户端通过扫描得到了服务端的BluetoothDevice对象之后,在BluetoothGattCallback对象里面定义以下回调:

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (status == BluetoothGatt.GATT_SUCCESS
            && newState == BluetoothGatt.STATE_CONNECTED){
        //通知服务端准备服务
        gatt.discoverServices();
    }
}

即连接上之后立即扫描服务端服务,服务端准备就绪之后,会在客户端触发以下回调:

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    //这里可以通过gatt获取到BluetoothGattService对象
    //通过BluetoothGattService可以获取BluetoothGattCharacteristic对象
}

至此,客户端与服务端的通讯必要对象就准备完毕了。

1.客户端向服务端发送数据

  • writeCharacteristic向服务端发送数据

客户端:

//data为byte[]数组,大小不能超过512Byte
characteristic.setValue(data);
//characteristic为要更新的BluetoothGattCharacteristic对象
writeCharacteristic(BluetoothGattCharacteristic)

方法来发送一个更新Characteristic的请求。
服务端会触发

onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value){
    //characteristic为要更新的BluetoothGattCharacteristic,但是其中的value并不是要更新的value
    //value才是新的值,客户端发送的数值
    characteristic.setValue(value);//服务端和客户端同步
    //要告诉客户端数据已经更新了
    //server是BluetoothGattServer实例
    server.sendResponse(device,requestId, BluetoothGatt.GATT_SUCCESS,offset,value);
}

接着,会在客户端触发

onCharacteristicWrite (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status){
    //如果在writeCharacteristic方法之前调用了
    //BluetoothGatt对象的beginReliableWrite()方法
    //此时服务端不会触发上述方法,但是会返回收到的数据
    //可以用之前的value在这里和characteristic的value比较,相同则数据完整
    //调用executeReliableWrite()才会触发服务端的回调
    //do Something...
}

至此,客户端向服务端发送数据的流程结束。

2.服务端向客户端发送数据

  • notifyCharacteristicChanged向客户端发送数据

首先在客户端注册characteristic的变化监听

gatt.setCharacteristicNotification(characteristic,true);

之后实现回调:

public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    Log.d("WDS","Notify");
    //characteristic已经是更改后的了,直接读取value即可
    Log.d("WDS",new String(characteristic.getValue()));
    //doSomething...
}

客户端即可收到服务端的特性改变通知。
服务端要通知的时候:

characteristic.setValue("2256".getBytes());
//server是BluetoothGattServer实例,device为客户端对象
server.notifyCharacteristicChanged(device,characteristic,true);

客户端获取到通知之后,会触发服务端的回调:

@Override
public void onNotificationSent(BluetoothDevice device, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS){
        //通知发送成功
    }
}

至此,服务端向客户端发送数据的流程结束。

由于某些原因,直接下载gradle文件很容易失败。我们可以手动下载工程对应版本的gradle文件
之后打开

C:\Users\用户名\.gradle\wrapper\dists\gradle-x.x-all\一串数字字母文件夹\ 

然后将下载的zip包放进去(不用改名)
重启Android Studio即可正常分析、编译工程了~

最近忙着折腾智能手表Android Wear2.0的开发,由于国内用不了play服务,所以谷歌为wear提供的一套和手机通信的API都不能用。为了在手机和手表之间传递数据,我不得不考虑使用蓝牙来建立手机和手表之间的通讯。在这个过程中被坑了许多次,所以记录下来方便自己查阅。
(PS: 国内有的教程使用的API已被弃用,许多东西是我自己看着官方文档总结出来的,如有不对的地方请帮我指出来。)


1.准备工作

工欲善其事,必先利其器。Android Wear2.0时的目标SDK版本已经在23以上了,安卓在6.0的时候加入了运行时权限系统,所以想开发蓝牙客户端的第一步,就是拿到应用所需要的必要权限。

需要的权限如下:

  • android.permission.BLUETOOTH
  • android.permission.BLUETOOTH_ADMIN
  • android.permission.ACCESS_COARSE_LOCATION

对于前两个权限,比较好理解,一个用于连接蓝牙设备,一个用于搜索蓝牙设备。可是这最后一个模糊地理位置权限是什么鬼?原来谷歌认为扫描周围蓝牙4.0设备可以获取用户的大概位置,所以要求开发者申请模糊位置权限。没有这个权限的话虽然能正常开始扫描蓝牙BLE设备,但是一个结果都不会返回,一点儿用都没有。

首先在AndroidManifest.xml之中声明权限:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

接着,需要在运行时申请权限:

//首先判断是否是安卓6.0以上,然后申请权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        this.requestPermissions(new String[]{
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.BLUETOOTH,
                Manifest.permission.ACCESS_COARSE_LOCATION
    },0);
}

之后在onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) 回调方法中确认用户是否同意了权限。


2.开启蓝牙BLE服务监听

开启蓝牙服务的步骤比较多,相比于客户端复杂了不少,感谢谷歌提供的全套文档,自己慢慢折腾也写出来了。
蓝牙服务端需要分别配置以下的类:

  • BluetoothGattCharacteristic 特性,最小的部分
  • BluetoothGattService 服务,里面包含特性
  • BluetoothGattServer 服务器,里面包含服务
  • BluetoothGattServerCallback 服务器回调,负责收发数据的方法写在里面

建立一个BLE服务器,需要设置好Service以及Service所包含的Characteristic。
这里只演示建立Service,通过Service的addCharacteristic方法可以添加Characteristic。

//服务的UUID
UUID SERVICE_UUID = UUID.fromString("…………自己的UUID…………");
//用UUID创建一个主服务
BluetoothGattService service = 
new BluetoothGattService(SERVICE_UUID,BluetoothGattService.SERVICE_TYPE_PRIMARY);
//创建服务的特性(characteristic)
characteristic = new BluetoothGattCharacteristic(
            CHARACTERISTIC_UUID,
            BluetoothGattCharacteristic.PROPERTY_WRITE |
                    BluetoothGattCharacteristic.PROPERTY_NOTIFY,
            BluetoothGattCharacteristic.PERMISSION_WRITE
            );
//为服务添加特性
gattService.addCharacteristic(characteristic);

接着创建服务器Server

//bm为BluetoothManager的实例。gattsCallback为服务器回调
BluetoothGattServer gattServer = bm.openGattServer(this,gattsCallback);
//添加服务
gattServer.addService(service);

这样就得到了一个gattServer对象,并且已经开始监听了,里面包含一个服务。


3.开启蓝牙BLE广播

单单开启服务监听并没有用,别人不知道你的设备是哪一个,所以需要开启广播,BLR服务端才能被别人发现。
开启BLE服务端广播需要以下几个类:

  • BluetoothLeAdvertiser 广播器,用于开始广播和停止广播。
  • AdvertiseSettings 广播的相关设置
  • AdvertiseData 广播的数据
  • AdvertiseCallback 广播的回调,用于通知开启广播是成功了还是失败了

首先创建AdvertiseSettings对象,这里用AdvertiseSettings.Builder来创建新的实例:

AdvertiseSettings.Builder settingBuilder = new AdvertiseSettings.Builder();
settingBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
//^这里将功率设置为最大,请根据需要选择合适的模式^
            .setConnectable(true)
//^广播出来自然是可连接的了,除非真是要根据周围蓝牙来确定用户模糊位置= =……^
            .setTimeout(0)
//^设置永不超时,直到手动停止广播^
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH);
//^同第一条,这里设置为最大功率方便查找^
AdvertiseSettings setting = settingBuilder.build();

然后设置AdvertiseData对象,用AdvertiseData.Builder来创建:

//bm: BluetootnManager实例
bm.getAdapter().setName("WATCH BLE");//设置设备名,用于广播识别
AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
dataBuilder.setIncludeDeviceName(true)
//^包含设备名称,adapter.setName()方法才能起作用^
            .setIncludeTxPowerLevel(false);
//^由于数据大小限制,不要广播无用数据,所以false^
AdvertiseData data = dataBuilder.build();

最后开始广播:

BluetoothLeAdvertiser advertiser = bm.getAdapter().getBluetoothLeAdvertiser();
//advertiseCallback是广播回调
advertiser.startAdvertising(setting,data,advertiseCallback);

这样就开启了BLE服务广播,可以被扫描到了,记得在BluetoothGattServerCallback回调连接成功之后,关闭广播。

低功耗蓝牙 BLE 客户端篇

最近忙着折腾智能手表Android Wear2.0的开发,由于国内用不了play服务,所以谷歌为wear提供的一套和手机通信的API都不能用。为了在手机和手表之间传递数据,我不得不考虑使用蓝牙来建立手机和手表之间的通讯。在这个过程中被坑了许多次,所以记录下来方便自己查阅。
(PS: 国内有的教程使用的API以被弃用,许多东西是我自己看着官方文档总结出来的,如有不对的地方请帮我指出来。)


1.准备工作

工欲善其事,必先利其器。Android Wear2.0时的目标SDK版本已经在23以上了,安卓在6.0的时候加入了运行时权限系统,所以想开发蓝牙客户端的第一步,就是拿到应用所需要的必要权限。

需要的权限如下:

  • android.permission.BLUETOOTH
  • android.permission.BLUETOOTH_ADMIN
  • android.permission.ACCESS_COARSE_LOCATION

对于前两个权限,比较好理解,一个用于连接蓝牙设备,一个用于搜索蓝牙设备。可是这最后一个模糊地理位置权限是什么鬼?原来谷歌认为扫描周围蓝牙4.0设备可以获取用户的大概位置,所以要求开发者申请模糊位置权限。没有这个权限的话虽然能正常开始扫描蓝牙BLE设备,但是一个结果都不会返回,一点儿用都没有。

首先在AndroidManifest.xml之中声明权限:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

接着,需要在运行时申请权限:

//首先判断是否是安卓6.0以上,然后申请权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        this.requestPermissions(new String[]{
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.BLUETOOTH,
                Manifest.permission.ACCESS_COARSE_LOCATION
    },0);
}

之后在onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) 回调方法中确认用户是否同意了权限。


2.查找蓝牙BLE设备

查找蓝牙设备要用到这样几个类:

  • BluetoothManager 蓝牙管理器
  • BluetoothAdapter 这个是我们本机的蓝牙设备
  • BluetoothLeScanner 低功耗蓝牙BLE扫描器
  • ScanCallback 扫描器需要使用的回调

基本过程就是,首先获取BluetoothManager,然后通过BluetoothManager获取到BluetoothAdapter,BluetoothAdapter获取到BluetoothLeScanner,用BluetoothLeScanner来注册回调并扫描蓝牙BLE设备。

BluetoothManager bm = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
BluetoothAdapter adapter = bm.getAdapter();
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
scanner.startScan(scanCallback);  //scanCallback是回调

如此便开始了扫描进程。接下来扫描的结果,会通过ScanCallback里面的
onScanResult(int callbackType, ScanResult result)方法返回,
result.getDevice()方法可以返回扫描到的蓝牙设备BluetoothDevice对象,这个就是我们下一步连接用的必要对象。


3.连接蓝牙BLE设备

连接蓝牙设备要用到这样几个类:

  • BluetoothDevice 蓝牙设备
  • BluetoothGatt 手机和蓝牙设备连接的“通道”
  • BluetoothGattCallback 和蓝牙设备通讯的回调,收发数据相关的代码都在这里实现

连接蓝牙BLE设备就比较容易了:

bluetoothDevice.connectGatt(this,false,gattCallback);

接着在gattCallback里面重写
onConnectionStateChange(BluetoothGatt gatt, int status, int newState)方法
判断当连接成功时,就可以保存gatt引用,使用方法传入的gatt对象和蓝牙BLE设备通讯了。
不过这里有个坑,gattCallback里面的回调并不是运行在主线程上面的,也就是说在回调之中不能直接操作UI对象,需要通知主线程来更改UI。