
暂无个人介绍
AES加密介绍 ASE 加密、解密的关键在于秘钥、只有使用加密时使用的秘钥,才可以解密。 生成秘钥的代码网上一大堆,下面的代码可生成一个秘钥 private SecretKey generateKey(String seed) throws Exception { // 获取秘钥生成器 KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); // 通过种子初始化 SecureRandom secureRandom = new SecureRandom(); secureRandom.setSeed(seed.getBytes("UTF-8")); keyGenerator.init(128, secureRandom); // 生成秘钥并返回 return keyGenerator.generateKey(); } 然后使用秘钥进行加密 private byte[] encrypt(String content, SecretKey secretKey) throws Exception { // 秘钥 byte[] enCodeFormat = secretKey.getEncoded(); // 创建AES秘钥 SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES"); // 创建密码器 Cipher cipher = Cipher.getInstance("AES"); // 初始化加密器 cipher.init(Cipher.ENCRYPT_MODE, key); // 加密 return cipher.doFinal(content.getBytes("UTF-8")); } 解密 private byte[] decrypt(byte[] content, SecretKey secretKey) throws Exception { // 秘钥 byte[] enCodeFormat = secretKey.getEncoded(); // 创建AES秘钥 SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES"); // 创建密码器 Cipher cipher = Cipher.getInstance("AES"); // 初始化解密器 cipher.init(Cipher.DECRYPT_MODE, key); // 解密 return cipher.doFinal(content); } 通常,如果加密和解密都是在同一个平台,比较简单,我们生成一个秘钥以后,将秘钥保存到本地,解密的时候直接获取本地的秘钥来解密就可以了,通常的使用场景为本地将xxx文件加密后上传保存或备份,需要的时候,下载再解密。这样上传的文件比较安全。 看上去很完美,下面问题来了,上述生产秘钥的方法,每次执行生成的秘钥都是不一样的。也就是说,加密时的秘钥如果没有保存到本地,解密的时候再次调用上述方法生成一个秘钥,那么将无法解密。 解决办法也有,使用如下方式生成秘钥,只要种子一样,生成的秘钥就是一样的。 private SecretKey generateKey(String seed) throws Exception { // 获取秘钥生成器 KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); // 通过种子初始化 SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", "Crypto"); secureRandom.setSeed(seed.getBytes("UTF-8")); keyGenerator.init(128, secureRandom); // 生成秘钥并返回 return keyGenerator.generateKey(); } 但是Android N(7.0)以后将不再支持,移除了Crypto。 E/System: ********** PLEASE READ ************ E/System: * E/System: * New versions of the Android SDK no longer support the Crypto provider. E/System: * If your app was relying on setSeed() to derive keys from strings, you E/System: * should switch to using SecretKeySpec to load raw key bytes directly OR E/System: * use a real key derivation function (KDF). See advice here : E/System: * http://android-developers.blogspot.com/2016/06/security-crypto-provider-deprecated-in.html E/System: *********************************** W/System.err: java.security.NoSuchProviderException: no such provider: Crypto Google也对应给出了解决方案,详见 Security “Crypto” provider deprecated in Android N 下面介绍另一种解决方案,我们不用种子生成秘钥,直接将password作为秘钥。 关于Android和IOS的同步问题,小伙伴也可以借鉴 AES加密 - iOS与Java的同步实现 如下方法 Android测试可行,IOS如果有小伙测试有问题也可以反馈给我。 加密 private byte[] encrypt(String content, String password) throws Exception { // 创建AES秘钥 SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES/CBC/PKCS5PADDING"); // 创建密码器 Cipher cipher = Cipher.getInstance("AES"); // 初始化加密器 cipher.init(Cipher.ENCRYPT_MODE, key); // 加密 return cipher.doFinal(content.getBytes("UTF-8")); } 解密 private byte[] decrypt(byte[] content, String password) throws Exception { // 创建AES秘钥 SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES/CBC/PKCS5PADDING"); // 创建密码器 Cipher cipher = Cipher.getInstance("AES"); // 初始化解密器 cipher.init(Cipher.DECRYPT_MODE, key); // 解密 return cipher.doFinal(content); } 注意:必须必须要注意的是,这里的password的长度,必须为128或192或256bits.也就是16或24或32byte。否则会报出如下错误: com.android.org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher$1: Key length not 128/192/256 bits. 至于数字、字母、中文都各自占几个字节,相信小伙伴的都是了解的,就不废话了。 也可以byte[] password = new byte[16/24/32]; 最后:至于最开始提到生成秘钥的方法,为什么种子相同,所生成的秘钥不同,还没看具体实现。有知道的小伙伴还请先指点一二。
CamShift算法基于色值,适用于追踪颜色和背景差异较大的目标。 效果图 以下调试代码,仅供参考: 源码 package com.kongqw; import android.graphics.Bitmap; import org.opencv.android.Utils; import org.opencv.core.Core; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfFloat; import org.opencv.core.MatOfInt; import org.opencv.core.Rect; import org.opencv.core.RotatedRect; import org.opencv.core.Scalar; import org.opencv.core.TermCriteria; import org.opencv.imgproc.Imgproc; import org.opencv.video.Video; import java.util.Collections; import java.util.List; import java.util.Vector; /** * Created by kongqingwei on 2017/4/26. * ObjectTracker */ public abstract class ObjectTracker { private Mat hsv, hue, mask, prob; private Rect trackRect; private RotatedRect rotatedRect; private Mat hist; private List<Mat> hsvList, hueList; private Bitmap bitmap; private MatOfFloat ranges; public abstract void onCalcBackProject(Bitmap prob); public ObjectTracker(Mat rgba) { hist = new Mat(); trackRect = new Rect(); rotatedRect = new RotatedRect(); hsvList = new Vector<>(); hueList = new Vector<>(); hsv = new Mat(rgba.size(), CvType.CV_8UC3); mask = new Mat(rgba.size(), CvType.CV_8UC1); hue = new Mat(rgba.size(), CvType.CV_8UC1); prob = new Mat(rgba.size(), CvType.CV_8UC1); bitmap = Bitmap.createBitmap(prob.width(), prob.height(), Bitmap.Config.ARGB_8888); ranges = new MatOfFloat(0f, 256f); } public Bitmap createTrackedObject(Mat mRgba, Rect region) { //将rgb摄像头帧转化成hsv空间的 rgba2Hsv(mRgba); updateHueImage(); Mat tempMask = mask.submat(region); // MatOfFloat ranges = new MatOfFloat(0f, 256f); MatOfInt histSize = new MatOfInt(25); List<Mat> images = Collections.singletonList(hueList.get(0).submat(region)); Imgproc.calcHist(images, new MatOfInt(0), tempMask, hist, histSize, ranges); Bitmap bitmap = Bitmap.createBitmap(hue.width(), hue.height(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(hue, bitmap); // 将hist矩阵进行数组范围归一化,都归一化到0~255 Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX); trackRect = region; return bitmap; } private void rgba2Hsv(Mat rgba) { Imgproc.cvtColor(rgba, hsv, Imgproc.COLOR_RGB2HSV); //inRange函数的功能是检查输入数组每个元素大小是否在2个给定数值之间,可以有多通道,mask保存0通道的最小值,也就是h分量 //这里利用了hsv的3个通道,比较h,0~180,s,smin~256,v,min(vmin,vmax),max(vmin,vmax)。如果3个通道都在对应的范围内,则 //mask对应的那个点的值全为1(0xff),否则为0(0x00). int vMin = 65, vMax = 256, sMin = 55; Core.inRange( hsv, new Scalar(0, sMin, Math.min(vMin, vMax)), new Scalar(180, 256, Math.max(vMin, vMax)), mask ); } private void updateHueImage() { hsvList.clear(); hsvList.add(hsv); // hue初始化为与hsv大小深度一样的矩阵,色调的度量是用角度表示的,红绿蓝之间相差120度,反色相差180度 hue.create(hsv.size(), hsv.depth()); hueList.clear(); hueList.add(hue); MatOfInt from_to = new MatOfInt(0, 0); // 将hsv第一个通道(也就是色调)的数复制到hue中,0索引数组 Core.mixChannels(hsvList, hueList, from_to); } public RotatedRect objectTracking(Mat mRgba) { rgba2Hsv(mRgba); updateHueImage(); // 计算直方图的反投影。 // Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, prob, ranges, 255); Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, prob, ranges, 1.0); // 计算两个数组的按位连接(dst = src1 & src2)计算两个数组或数组和标量的每个元素的逐位连接。 Core.bitwise_and(prob, mask, prob, new Mat()); // 追踪目标 rotatedRect = Video.CamShift(prob, trackRect, new TermCriteria(TermCriteria.EPS, 10, 1)); // 将本次最终到的目标作为下次追踪的对象 trackRect = rotatedRect.boundingRect(); rotatedRect.angle = -rotatedRect.angle; Imgproc.rectangle(prob, trackRect.tl(), trackRect.br(), new Scalar(255, 255, 0, 255), 6); Utils.matToBitmap(prob, bitmap); onCalcBackProject(bitmap); return rotatedRect; } } 使用部分 public Mat onCameraFrame(CvCameraViewFrame inputFrame) { mRgba = inputFrame.rgba(); mGray = inputFrame.gray(); if (null == objectTracker) { objectTracker = new ObjectTracker(mRgba) { @Override public void onCalcBackProject(final Bitmap prob) { MainActivity.this.runOnUiThread(new Runnable() { @Override public void run() { imageView.setImageBitmap(prob); } }); } }; } if (null != mTrackWindow) { Log.i(TAG, "onCameraFrame: objectTracker = " + objectTracker + " mTrackWindow = " + mTrackWindow); RotatedRect rotatedRect = objectTracker.objectTracking(mRgba); Imgproc.ellipse(mRgba, rotatedRect, FACE_RECT_COLOR, 6); Rect rect = rotatedRect.boundingRect(); Imgproc.rectangle(mRgba, rect.tl(), rect.br(), FACE_RECT_COLOR, 3); } // System.gc(); return mRgba; } int xDown; int yDown; @Override public boolean onTouch(View v, MotionEvent event) { int cols = mRgba.cols(); int rows = mRgba.rows(); int xOffset = (mOpenCvCameraView.getWidth() - cols) / 2; int yOffset = (mOpenCvCameraView.getHeight() - rows) / 2; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: xDown = (int) event.getX() - xOffset; yDown = (int) event.getY() - yOffset; break; case MotionEvent.ACTION_UP: int xUp = (int) event.getX() - xOffset; int yUp = (int) event.getY() - yOffset; // 获取跟踪目标 mTrackWindow = new Rect(Math.min(xDown, xUp), Math.min(yDown, yUp), Math.abs(xUp - xDown), Math.abs(yUp - yDown)); // 创建跟踪目标 Bitmap bitmap = objectTracker.createTrackedObject(mRgba, mTrackWindow); imageView.setImageBitmap(bitmap); Toast.makeText(getApplicationContext(), "已经选中跟踪目标!", Toast.LENGTH_SHORT).show(); break; default: break; } return true; } 参考 【计算机视觉】人脸实时跟踪1——利用CamShift来跟踪人脸 目标跟踪–CamShift
工厂模式 工厂模式,对于大部分的开发者来讲,可能并不陌生,但是可能并非所有人都能完全理解,尤其是对于初学者。有的初学者经常会问: 工厂模式有什么用?有什么好处?什么时候用工厂模式? 网络上也有很多解答,诸如:可以更好的封装、模块化、还有的书上写着在生成复杂对象的地方,可以使用工厂模式。那么如何衡量一个对象的复杂的?这些解释都是对的,但是对于一个初学者并不好理解。 对于初学者,最大的疑惑可能就在于:通过工厂create出来的对象和new出来的对象相比,优势在哪里?直接new不就好了吗,还要写个工厂类这么麻烦? 笔者也是一直处于学习过程,梳理了一下工厂模式的使用场景和解决的痛点,通俗的聊一聊,如有错误,欢迎指正! 抛开前面抽象的定义,举一个通俗的例子,带你自然而然的应用上工厂设计模式。 某团队有如下两位开发人员: 移动端 : 小张 算法 : 小李 小张负责终端应用的开发,小李根据具体的业务逻辑写一些算法,封装成SDK提供给小张使用。 某天,产品提出了需求,需要开发某功能,算法小李根据需求,封装了一个类,提供了一个1.0版本的SDK给终端小张使用。具体如下 public class FunctionA{ public void method(){ // 业务逻辑 } } 小张可以在程序里直接调用方法而不用管小李是如何实现的 FunctionA functionA = new FunctionA(); functionA.method(); 两人各司其职,合作非常愉快。如果你也觉得,恩,他们配合的不错,那么你可以继续往下看。 直到有一天,产品又提出了新的需求,以前的算法逻辑不用了,有了新的逻辑。 算法小李最开始想到的就是,OK,改一下FunctionA的method()方法,改为新的。但是小李一想,如果某一天万恶的产品又要改回去怎么办?想想不行,之前的算法不能删,于是乎,算法小李又重新定义了一个类,实现了第二套业务逻辑,生成了1.1版本的SDK给终端小张使用,果然高明。 public class FunctionB{ public void method(){ // 新的业务逻辑 } } 终端小张收到算法小李给的SDK以后发现,根据需求,以前的FunctionA不能再用了,于是乎,终端小张也开始改程序,使用FunctionB FunctionB functionB = new FunctionB(); functionB.method(); 终端小张开始有点不爽,你改你的算法,和我有什么关系?增加我的工作量! (试想一下,如果你使用了某第三方SDK,SDK每次更新,你在本地都要改代码,是不是得疯掉?) 产品又来了,改改改…… 改了好多次,小张和小李打了起来…… 算法小李应该怎样做,可以避免这种情况?这里就可以使用到工厂方法。 算法FunctionA和算法FunctionB都是为了实现同一个功能,只不过实现过程不一样。 终端小张想要的是: 你改你的算法,你改完算法,我可以替换新的SDK,但是不要影响到我本地的程序。 我只要调用function.method();就好了。 我不管你function是算法A还是算法B。 那么,这里小张想要的function就涉及到了Java的多态了,它即可能是FunctionA也可能是FunctionB。 算法小李就可以这样改,定义一个抽象的Function,有一个抽象的method()方法,然后让算法FunctionA和算法FunctionB都继承Function,分别实现method()方法。 像这样 Function public abstract class Function { public abstract void method(); } FunctionA public class FunctionA extends Function { @Override public void method() { // 算法A的实现 } } FunctionB public class FunctionB extends Function { @Override public void method() { // 算法B的实现 } } 然后再提供一个工厂类,可以返回一个多态的Function对象 public class FunctionFactory { public static Function createFunction() { return new FunctionB(); } } 这样一来,终端小张爽了,可以通过工厂获取Function Function function = FunctionFactory.createFunction(); function.method(); 以后算法不管怎样改,他都不需要再担心,如果产品再想改算法,算法小李只需要再定义一个FunctionC,在createFunction方法里返回即可。 这也体现了为什么说工厂模式是依赖于抽象架构的,他们都有属于一类事物,都有相同的方法,只是实现不同,由此可见,通过工厂模式,可以实现解耦,对于算法小李,还有一定的可扩展性。 又有一天,产品又来新需求了,希望用户可以自己选择使用哪种逻辑算法。 终端小张希望可以提供相应接口,对应提供不同的逻辑。 算法小李首先想到的就是再多写几个create方法,诸如createFunctionA(),createFunctionB()…… 虽然不是不行,但是假如你有成百上千套算法,难道要写成百上千个create方法么,这显然是码农的做法,作为工程师,我想只用一个方法,指哪打哪,要啥给啥!这就用到了反射。 可以这样实现 FuncFactory public abstract class FuncFactory { public abstract <T extends Function> T createFunction(Class<T> clz); } FunctionFactory public class FunctionFactory extends FuncFactory { @Override public <T extends Function> T createFunction(Class<T> clz) { Function function = null; try { function = (Function) Class.forName(clz.getName()).newInstance(); } catch (Exception e) { e.printStackTrace(); } return (T) function; } } 这样一来,终端小张就可以尽情玩耍了, 想用A算法,就传一个FunctionA FuncFactory factory = new FunctionFactory(); Function function = factory. createFunction(FunctionA.class); 想用算法B,就传一个FunctionB FuncFactory factory = new FunctionFactory(); Function function = factory. createFunction(FunctionB.class); 这里用到了抽象工厂类FuncFactory,同样也是为了方便扩展,现在小张和小李只用到了一种逻辑,可有可无,如果以后还有多种逻辑要实现,可以在抽象工厂里添加对应的create方法,方便扩展。 简单的理解到这里,欢迎指正!
Builder模式 定义 将一个复杂对象的构建过程分离,使得同样的构建过程可以创建不同的结果。 使用场景 相同的方法,不同的执行顺序,产生不同的结果 多个零件或者部件,可以组装到一个对象中,产生不同的结果 初始化过程比较复杂,参数较多 举一个例子,Android对话框,就是Builder模式,像这样: new AlertDialog.Builder(this) .setTitle("xxx") .setMessage("xxx") .create() .show(); 本是一个相对复杂的对话框,通过链式编程构建出来。 实现 我们假设组装一台宝马轿车,它有品牌、型号、颜色。 车 public abstract class Car { // 品牌 protected String mBrand; // 型号 protected String mModel; // 颜色 protected String mColor; public abstract void setBrand(); public void setColor(String mColor) { this.mColor = mColor; } public void setModel(String model) { this.mModel = model; } @Override public String toString() { return "Car{" + "mBrand='" + mBrand + '\'' + ", mModel='" + mModel + '\'' + ", mColor='" + mColor + '\'' + '}'; } } 宝马车 public class BMWCar extends Car { @Override public void setBrand() { mBrand = "BMW"; } } 下面是Builder类,用来描述构建一台车需要哪些方法 public abstract class CarBuilder { public abstract CarBuilder buildBrand(); public abstract CarBuilder buildModel(String model); public abstract CarBuilder buildColor(String color); public abstract Car create(); } 接下来来实现构建一台宝马车的Builder public class BMWBuilder extends CarBuilder { Car car = new BMWCar(); @Override public BMWBuilder buildBrand() { car.setBrand(); return this; } @Override public BMWBuilder buildModel(String model) { car.setModel(model); return this; } @Override public BMWBuilder buildColor(String color) { car.setColor(color); return this; } @Override public Car create() { return car; } } 这里的build方法都返回自身,用来链式调用。 调用 下面来构建一台红色的3系宝马轿车 new BMWBuilder() .buildBrand() .buildModel("3 Li") .buildColor("红色") .create(); 如果你还想给车子添加个HUD、换个真皮座椅,只需要在Builder里添加对用的方法,构建的时候设置属性就可以了。 结论 Builder设计模式具有良好的封装性,构建者独立,方便扩展。 不足就是需要创建多个Builder。
单例模式 定义 确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例。 使用场景 确保某个类有且只有一个,避免产生过多对象消耗过多的资源,比如,太阳只有一个,地球只有一个…… 关键点 构造函数不对外开放,一般为private 通过一个静态方法或者枚举返回单例类对象 对象有且只有一个,尤其是在多线程下 确保在反序列的时候不会重复构建对象 实现 饿汉单例模式 public class HungerSingleton { private static final String TAG = "HungerSingleton"; private static final HungerSingleton mHungerSingleton = new HungerSingleton(); private HungerSingleton() { Log.i(TAG, "HungerSingleton: "); } public static HungerSingleton getInstance() { return mHungerSingleton; } @Override public String toString() { return "I am " + TAG + "!"; } } 懒汉单例模式 优点:使用的时候才进行初始化,节约了资源 缺点:第一次初始化较慢,每次都同步造成不必要的开销 结论:不推荐使用 public class LazySingleton { private static final String TAG = "LazySingleton"; private static LazySingleton mLazySingleton; private LazySingleton() { Log.i(TAG, "LazySingleton: "); } /** * 获取单例 * synchronized 确保在多线程下对象唯一行 * * @return 单例 */ public static synchronized LazySingleton getInstance() { if (null == mLazySingleton) { mLazySingleton = new LazySingleton(); } return mLazySingleton; } @Override public String toString() { return "I am " + TAG + "!"; } } Double Check Lock(DCL)单例模式 优点:在懒汉单例的基础上,不会多次执行同步操作,资源利用率高,效率高。 缺点:第一次加载较慢 结论:使用最多的单例实现方式 public class DCLSingleton { private static final String TAG = "DCLSingleton"; private volatile static DCLSingleton mDCLSingleton = null; private DCLSingleton() { Log.i(TAG, "DCLSingleton: "); } public static DCLSingleton getInstance() { if (null == mDCLSingleton) { // 避免重复不必要的同步 synchronized (DCLSingleton.class) { // 确保多线程下对象唯一 if (null == mDCLSingleton) { // 非空的情况下创建实例 mDCLSingleton = new DCLSingleton(); } } } return mDCLSingleton; } @Override public String toString() { return "I am " + TAG + "!"; } } 静态内部类单例模式 优点:线程安全,保证对象唯一,延迟了实例化, 结论:推荐使用 public class StaticInnerClassSingleton { private static final String TAG = "StaticInnerClassSinglet"; private StaticInnerClassSingleton() { Log.i(TAG, "StaticInnerClassSingleton: "); } public static StaticInnerClassSingleton getInstance() { return SingletonHolder.staticInnerClassSingleton; } private static class SingletonHolder { private static final StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } @Override public String toString() { return "I am " + TAG + "!"; } } 结论 构造方法私有化 通过静态方法获取一个唯一的实例 保证线程安全 单例对象如果持有Context,很容易造成内存泄漏,最好传递ApplicationContext
Android Debug Bridge 本文以MAC OS为例 ADB版本 $ ./adb version 如果出现 $ ./adb version -bash: ./adb: No such file or directory 说明你还没有配置环境变量 你可以来到ADB的目录下再执行命令,目录在 $ cd Library/Android/sdk/platform-tools/ 也可以自行google配置一下环境变量 这样就可以在任意目录使用ADB指令 终端开启Debug模式 在设置中找到开发者选项,并开启debug模式 开发者选项默认是隐藏状态,如何开启自行google 查看设备 $ ./adb devices List of devices attached 4f0a34ac device 5F6F7108 device 前面是序列号,后面是设备状态 状态 说明 device 实例现在已连接到 adb 服务器。请注意,此状态并不表示 Android 系统已完全启动且可以运行,因为在此实例连接到 adb 时系统仍在启动。不过,在启动后,这将是模拟器/设备实例的正常运行状态。 offline 实例未连接到 adb 或不响应。 no device 未连接模拟器/设备。 发送指令到指定终端 $ ./adb -s 序列号 指令 参数 说明 注释 -s 序列号 将 adb 命令发送至以其 adb 分配的序列号命名的特定模拟器/设备实例(如“emulator-5556”)。 -d 将 adb 命令发送至唯一连接的 USB 设备。 如果连接了多个 USB 设备,将返回错误。 -e 将 adb 命令发送至唯一运行的模拟器实例。 如果有多个模拟器实例在运行,将返回错误。 安装应用 多个设备 $ ./adb -s 序列号 install xxx/xxx/xxx.apk 单个真机设备 $ ./adb -d install xxx/xxx/xxx.apk 单个模拟器 $ ./adb -e install xxx/xxx/xxx.apk 从PC端复制文件到终端 $ ./adb pull `终端文件绝对路径` `PC端文件绝对路径` e.g.:将手机SD下的demo.mp4文件拷贝到桌面 $ ./adb -d pull /sdcard/demo.mp4 /Users/kongqingwei/Desktop [100%] /sdcard/demo.mp4 从终端复制文件到PC端 $ ./adb -d push `PC端文件绝对路径` `终端文件绝对路径` e.g.:将桌面的图片拷贝到终端SD卡 $ ./adb -d push /Users/kongqingwei/Desktop/ic_launcher.png /sdcard/ic_launcher.png [100%] /sdcard/ic_launcher.png 关闭ADB 终止 adb 服务器进程。 当adb无响应的时候可以关闭adb再重启,得以解决问题。 $ ./adb kill-server 启动ADB 检查 adb 服务器进程是否在运行,如果未运行则启动它。 $ ./adb start-server * daemon not running. starting it now on port 5037 * * daemon started successfully * 进入终端shell $ ./adb [-d|-e|-s `序列号`] shell `shell指令` 或 $ ./adb [-d|-e|-s `序列号`] shell e.g. $ ./adb -d shell ls acct bin cache config d data default.prop …… 或 $ ./adb -d shell shell@mocha:/ $ 退出终端shell shell@mocha:/ $ exit 打印Logcat $ ./adb logcat [<option>] ... [<filter-spec>] ... 或 $ ./adb shell [<option>] ... [<filter-spec>] ... $ logcat 详细参数 截屏 截屏保存到/sdcard/screen.png $ ./adb -d shell screencap /sdcard/screen.png 录屏 按 Control + C 停止屏幕录制,否则,到三分钟或 –time-limit 设置的时间限制时,录制将自动停止。 $ ./adb -d shell screenrecord /sdcard/demo.mp4 ^C 参数 说明 –help 显示命令语法和选项 –size widthxheight 设置视频大小:1280x720。默认值是设备的原生显示分辨率(如果支持),如果不支持,则使用 1280x720。为实现最佳结果,请使用设备的 Advanced Video Coding (AVC) 编码器支持的大小。 –bit-rate rate 设置视频的视频比特率(以兆比特每秒为单位)。默认值为 4Mbps。您可以增加比特率以提升视频质量,但这么做会导致影片文件变得更大。以下示例将录制比特率设为6Mbps:screenrecord --bit-rate 6000000 /sdcard/demo.mp4 –time-limit time 设置最大录制时长(以秒为单位)。默认值和最大值均为 180(3 分钟)。 –rotate 将输出旋转 90 度。此功能是实验性的。 –verbose 显示命令行屏幕上的日志信息。如果您不设置此选项,则运行时此实用程序不会显示任何信息。 Activity Manager (am) 软件包管理器 (pm)
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN To get a Git project into your build: Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end of repositories: allprojects { repositories { ... maven { url 'https://jitpack.io' } } } Step 2. Add the dependency dependencies { compile 'com.github.kongqw:AndroidBluetoothManager:1.0.0' } AndroidBluetoothManager 效果图 PNG GIF 基础功能 添加权限 <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 初始化 mBluetoothManager = new BluetoothManager(); 打开蓝牙 mBluetoothManager.openBluetooth(); 关闭蓝牙 mBluetoothManager.closeBluetooth(); 添加蓝牙开关状态的监听 mBluetoothManager.setOnBluetoothStateListener(this); /** * 正在关闭蓝牙的回调 */ @Override public void onBluetoothStateTurningOff() { // TODO } /** * 蓝牙关闭的回调 */ @Override public void onBluetoothStateOff() { // TODO } /** * 正在打开蓝牙的回调 */ @Override public void onBluetoothStateTurningOn() { // TODO } /** * 蓝牙打开的回调 */ @Override public void onBluetoothStateOn() { // TODO } 移除蓝牙开关状态的监听 mBluetoothManager.removeOnBluetoothStateListener(); 设置蓝牙可见 startActivity(mBluetoothManager.getDurationIntent(0)); 获取蓝牙名称 mBluetoothManager.getName() 修改蓝牙名称 mBluetoothManager.setName(newName); 扫描附近的蓝牙设备 mBluetoothManager.discovery(); 添加扫描蓝牙设备的监听 mBluetoothManager.setOnDiscoveryDeviceListener(this); /** * 开始扫描附近蓝牙设备的回调 */ @Override public void onDiscoveryDeviceStarted() { // TODO } /** * 扫描到附近蓝牙设备的回调 * * @param device 蓝牙设备 */ @Override public void onDiscoveryDeviceFound(BluetoothDevice device) { // TODO } /** * 扫描附近蓝牙设备完成的回调 */ @Override public void onDiscoveryDeviceFinished() { // TODO } 移除扫描蓝牙设备的监听 mBluetoothManager.removeOnDiscoveryDeviceListener(); 服务端 初始化 mBluetoothService = new BluetoothService() { @Override protected UUID onSecureUuid() { // TODO 设置自己的UUID return UUID_SECURE; } @Override protected UUID onInsecureUuid() { // TODO 设置自己的UUID return UUID_INSECURE; } }; 等待客户端连接 mBluetoothService.start(); 断开连接/释放资源 mBluetoothService.stop(); 添加蓝牙连接的监听 mBluetoothService.setOnServiceConnectListener(new OnServiceConnectListener() { @Override public void onConnectListening() { // TODO } @Override public void onConnectSuccess(BluetoothDevice device) { // TODO } @Override public void onConnectFail(Exception e) { // TODO } @Override public void onConnectLost(Exception e) { // TODO } }); 发送消息 mBluetoothService.send(chatText); 添加消息收发的监听 mBluetoothClient.setOnMessageListener(this); /** * 蓝牙发送了消息 * * @param message 发送的消息 */ @Override public void onSend(String message) { // TODO } /** * 蓝牙接收到消息 * * @param message 接收的消息 */ @Override public void onRead(String message) { // TODO } 客户端 初始化 mBluetoothClient = new BluetoothClient() { @Override protected UUID onSecureUuid() { // TODO 设置自己的UUID return UUID_SECURE; } @Override protected UUID onInsecureUuid() { // TODO 设置自己的UUID return UUID_INSECURE; } }; 蓝牙连接(安全) mBluetoothClient.connect(mBluetoothDevice, true); 蓝牙连接(不安全) mBluetoothClient.connect(mBluetoothDevice, false); 断开连接/释放资源 mBluetoothClient.stop(); 添加蓝牙连接的监听 mBluetoothClient.setOnClientConnectListener(new OnClientConnectListener() { @Override public void onConnecting() { // TODO } @Override public void onConnectSuccess(BluetoothDevice device) { // TODO } @Override public void onConnectFail(Exception e) { // TODO } @Override public void onConnectLost(Exception e) { // TODO } }); 发送消息 mBluetoothClient.send(chatText); 添加消息收发的监听 mBluetoothClient.setOnMessageListener(this); /** * 蓝牙发送了消息 * * @param message 发送的消息 */ @Override public void onSend(String message) { // TODO } /** * 蓝牙接收到消息 * * @param message 接收的消息 */ @Override public void onRead(String message) { // TODO }
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN Android 雷达扫描控件 To get a Git project into your build: Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end of repositories: allprojects { repositories { ... maven { url 'https://jitpack.io' } } } Step 2. Add the dependency dependencies { compile 'com.github.kongqw:AndroidRadarScanView:1.0.1' } 源码:AndroidRadarScanView 效果图 XML <com.kongqw.radarscanviewlibrary.RadarScanView android:id="@+id/radarScanView" android:layout_width="match_parent" android:layout_height="match_parent" /> 初始化 radarScanView = (RadarScanView) findViewById(R.id.radarScanView); 设置属性 XML xmlns:app="http://schemas.android.com/apk/res-auto" <com.kongqw.radarscanviewlibrary.RadarScanView android:id="@+id/radarScanView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" app:radarBackgroundColor="@color/colorAccent" app:radarBackgroundLinesColor="@color/colorPrimaryDark" app:radarBackgroundLinesNumber="3" app:radarBackgroundLinesWidth="5.5" app:radarScanAlpha="0x33" app:radarScanColor="#FF000000" app:radarScanTime="5000" /> 属性 类型 描述 radarScanTime integer 设置雷达扫描一圈时间 radarBackgroundLinesNumber integer 设置雷达背景圆圈数量 radarBackgroundLinesWidth float 设置雷达背景圆圈宽度 radarBackgroundLinesColor color 设置雷达背景圆圈颜色 radarBackgroundColor color 设置雷达背景颜色 radarScanColor color 设置雷达扫描颜色 radarScanAlpha integer 设置雷达扫描透明度 Java radarScanView // 设置雷达扫描一圈时间 .setRadarScanTime(2000) // 设置雷达背景颜色 .setRadarBackgroundColor(Color.WHITE) // 设置雷达背景圆圈数量 .setRadarBackgroundLinesNumber(4) // 设置雷达背景圆圈宽度 .setRadarBackgroundLinesWidth(2) // 设置雷达背景圆圈颜色 .setRadarBackgroundLinesColor(Color.GRAY) // 设置雷达扫描颜色 .setRadarScanColor(0xFFAAAAAA) // 设置雷达扫描透明度 .setRadarScanAlpha(0xAA); 备用 手动开始扫描 radarScanView.startScan(); 手动停止扫描 radarScanView.stopScan();
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN Gradle http://google.github.io/android-gradle-dsl/current/index.html https://docs.gradle.org/current/userguide/java_plugin.html 打包多个版本 开发过程中我们经常需要打包多个版本的apk,最为常见的,一个是release版本,一个是debug版本,他们可能使用的api也有所区别,手动改起来总是很麻烦。 我们可以通过Gradle,配置多个版本,他们有各自的参数来区分不同的版本。如下,在 app/build.gradle 系统默认会给我生成release版本,我们可以手动自己添加一个版本,我这里命名为debug,分别添加了三种类型的参数。 apply plugin: 'com.android.application' android { …… buildTypes { release { …… buildConfigField("boolean", "isDebug", "false") } debug { // 添加了boolean类型的参数 buildConfigField("boolean", "isDebug", "true") // 添加了String类型的参数 buildConfigField("String", "coder", "\"kongqw\"") // 添加了int类型的参数 buildConfigField("int", "age", "26") } } } …… dependencies { …… } 添加完成后Rebuild,会在 BuildConfig 下看到我们添加的参数 因为是静态变量,取值时直接用类名点变量名即可 上述属于在Java代码中添加字段,同样的,Gradle也支持添加xml属性,类似这样 apply plugin: 'com.android.application' android { …… defaultConfig { …… } buildTypes { release { …… } debug { …… resValue("bool", "is_debug", "true") resValue("string", "coder", "\"kongqw\"") resValue("integer", "age", "26") } } } dependencies { …… } 添加完以后Rebuild,会在generated.xml 下生产如下字段 但是要避免和string.xml文件里的字段重复 在xml中使用 android:text="@string/coder" 或者再Java中使用 String coder = getString(R.string.coder); 添加工程build时间 有时候,测试和产品总是会拿着手机跑过来问你,“有没有更新?”、“帮我看一下我装的是不是最新的版本?”,总是很烦,我们可以利用Gradle,获取到工程的Build时间,在测试版本打印出来,可以为工程师节省不少时间。 Gradle支持直接添加方法 apply plugin: 'com.android.application' android { …… defaultConfig { …… resValue("string", "build_time", getDate()) // BuildTime buildConfigField("String", "buildTime", "\"" + getDate() + "\"") } buildTypes { release { …… } debug { …… } } } def getDate() { return new Date().format("yyyy-MM-dd HH:mm:ss") } dependencies { …… } 判断的时候调用 BuildConfig.buildTime 或者 getString(R.string.build_time) 不同版本包名不同 我们都知道,相同包名,不同签名,在一台手机上是无法同时安装的。前面我们配置了两个不同的版本,release版本肯定是用正式签名,debug版本我们通常都会使用测试签名。那么问题来了,测试人员如果已经在手机上安装了上线的版本,再装测试版,就会有冲突,必须要卸载一个,那么最好的办法就是修改测试版包名。这样包名不同,两个版本同时安装,也不会有冲突。 同样的,Gradle依然可以轻松的帮我们做到。 apply plugin: 'com.android.application' android { …… defaultConfig { …… } buildTypes { release { …… } debug { …… applicationIdSuffix ".debug" } } } dependencies { …… } 可以看到我们在 debug 版本添加了applicationIdSuffix,其值为 .debug,顾名思义,是debug版本在包名后面添加.debug后缀。 多渠道打包 多渠道打包,打包多个市场的apk,用来统计。 首先在AndroidManifest文件添加meta-data <?xml version="1.0" encoding="utf-8"?> <manifest ……> <application ……> <meta-data android:name="PRODUCT" android:value="${CHANNEL_VALUE}"/> <activity ……> …… </activity> </application> </manifest> 在app/build.gradle添加productFlavors,如下所示,添加 XIAO_MI和GOOGLE_PLAY两个渠道 apply plugin: 'com.android.application' android { …… defaultConfig { …… } buildTypes { release { …… } debug { …… } } productFlavors { xiaomi { manifestPlaceholders = [CHANNEL_VALUE: "XIAO_MI"] } googlePlay { manifestPlaceholders = [CHANNEL_VALUE: "GOOGLE_PLAY"] } } } dependencies { …… } 获取渠道 try { ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); String channel = applicationInfo.metaData.getString("PRODUCT"); Log.i(TAG, "onCreate: 渠道 :" + channel); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); }
历史遗留问题终该解决,之前有文章Android连接WIFI,今天再次整理一下,梳理一下遗留的问题 修改或者删除配置失败,返回-1 Android 6.0以后的限制,程序本身只能修改和删除自己创建的配置,如果是在手机WIFI管理器或者其他应用程序连接的WIFI,那么只能连接,不能修改(有ROOT权限除外)。 getScanResults()返回空 Android 6.0动态权限问题,需要添加并动态校验ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限,权限通过以后就可以获取到WIFI列表。 WIFI连接成功以后获取SSID为<unknown ssid> 在监听到SUPPLICANT_STATE_CHANGED_ACTION的广播以后获取SupplicantState状态,当状态为COMPLETED以后,再通过WifiManager的getConnectionInfo()方法获取SSID。 源码:KqwWifiManagerDemo
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN 效果图 GitHub地址:PermissionsManager 随着Android 6.0的普及,动态权限的重要性也开始时慢慢体现出来。为了更好的保护用户隐私,Android 6.0要求在进行敏感操作之前,必须要向用户请示申请权限。 如何使用,在之前的文章里也已经介绍过了,但是用起来比较麻烦。Android6.0动态获取权限 我希望可以封装一下,使用之前创建一个动态权限的管理对象,他有两个回调来告诉我权限申请成功或者失败,像这样: mPermissionsManager = new PermissionsManager(this) { @Override public void authorized(int requestCode) { // TODO 权限通过 } @Override public void noAuthorization(int requestCode, String[] lacksPermissions) { // TODO 有权限没有通过 } }; 使用的时候,可以直接调用一个方法,把要请示的权限传进去就可以进行校验,像这样: // 检查权限 mPermissionsManager.checkPermissions("请求码", "要校验的权限"); 于是乎,下面封装的动态权限管理器就来了: 动态权限管理器 package com.kongqw.permissionslibrary; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import java.util.ArrayList; /** * Created by kongqingwei on 2017/2/15. * 检查权限的类 */ public abstract class PermissionsManager { private static final String PACKAGE_URL_SCHEME = "package:"; private Activity mTargetActivity; public abstract void authorized(int requestCode); public abstract void noAuthorization(int requestCode, String[] lacksPermissions); public PermissionsManager(Activity targetActivity) { mTargetActivity = targetActivity; } /** * 检查权限 * * @param requestCode 请求码 * @param permissions 准备校验的权限 */ public void checkPermissions(int requestCode, String... permissions) { ArrayList<String> lacks = new ArrayList<>(); for (String permission : permissions) { if (ContextCompat.checkSelfPermission(mTargetActivity.getApplicationContext(), permission) == PackageManager.PERMISSION_DENIED) { lacks.add(permission); } } if (!lacks.isEmpty()) { // 有权限没有授权 String[] lacksPermissions = new String[lacks.size()]; lacksPermissions = lacks.toArray(lacksPermissions); //申请CAMERA权限 ActivityCompat.requestPermissions(mTargetActivity, lacksPermissions, requestCode); } else { // 授权 authorized(requestCode); } } /** * 复查权限 * <p> * 调用checkPermissions方法后,会提示用户对权限的申请做出选择,选择以后(同意或拒绝) * TargetActivity会回调onRequestPermissionsResult方法, * 在onRequestPermissionsResult回调方法里,我们调用此方法来复查权限,检查用户的选择是否通过了权限申请 * * @param requestCode 请求码 * @param permissions 权限 * @param grantResults 授权结果 */ public void recheckPermissions(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { for (int grantResult : grantResults) { if (grantResult == PackageManager.PERMISSION_DENIED) { // 未授权 noAuthorization(requestCode, permissions); return; } } // 授权 authorized(requestCode); } /** * 进入应用设置 * * @param context context */ public static void startAppSettings(Context context) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(Uri.parse(PACKAGE_URL_SCHEME + context.getPackageName())); context.startActivity(intent); } } 应用 使用起来的逻辑也比较清晰简单,一共3步: 1. 初始化权限管理器 // 初始化 mPermissionsManager = new PermissionsManager(this) { @Override public void authorized(int requestCode) { Toast.makeText(getApplicationContext(), requestCode + " : 全部权限通过", Toast.LENGTH_SHORT).show(); } @Override public void noAuthorization(int requestCode, String[] lacksPermissions) { Toast.makeText(getApplicationContext(), requestCode + " : 有权限没有通过!需要授权", Toast.LENGTH_SHORT).show(); for (String permission : lacksPermissions) { Log.i(TAG, "noAuthorization: " + permission); } } }; 2. 检查权限 // 要校验的权限 String[] PERMISSIONS = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA}; // 检查权限 mPermissionsManager.checkPermissions(0, PERMISSIONS); 3. 复查权限 用户对权限申请的提示做出选择以后,要重写TargetActivity的onRequestPermissionsResult方法来复查权限,检查权限是否通过。 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // 用户做出选择以后复查权限,判断是否通过了权限申请 mPermissionsManager.recheckPermissions(requestCode, permissions, grantResults); } 进入应用设置页面 最后,权限没有通过,是不能使用的,如果一定要用,一般要提示用户缺少权限,到应用设置页面去吧权限打开,再回来使用。 对话框就不写了,进入到应用的设置页面可以直接调用PermissionsManager里的startAppSettings方法,进入到该应用的设置页面,修改权限 PermissionsManager.startAppSettings(getApplicationContext());
TCP和UDP区别 文章转自TCP和UDP区别 - TCP UDP 是否连接 面向连接 面向非连接 传输可靠性 可靠的 不可靠的 应用场合 传输大量的数据 少量数据 速度 慢 快 OSI 和 TCP/IP 模型在传输层定义两种传输协议:TCP(或传输控制协议)和 UDP(或用户数据报协议)。 UDP UDP 与 TCP 的主要区别在于 UDP 不一定提供可靠的数据传输。 事实上,该协议不能保证数据准确无误地到达目的地。UDP 在许多方面非常有效。当某个程序的目标是尽快地传输尽可能多的信息时(其中任意给定数据的重要性相对较低),可使用 UDP。ICQ 短消息使用 UDP 协议发送消息。 许多程序将使用单独的TCP连接和单独的UDP连接。重要的状态信息随可靠的TCP连接发送,而主数据流通过UDP发送。 TCP TCP的目的是提供可靠的数据传输,并在相互进行通信的设备或服务之间保持一个虚拟连接。TCP在数据包接收无序、丢失或在交付期间被破坏时,负责数据恢复。它通过为其发送的每个数据包提供一个序号来完成此恢复。记住,较低的网络层会将每个数据包视为一个独立的单元,因此,数据包可以沿完全不同的路径发送,即使它们都是同一消息的组成部分。这种路由与网络层处理分段和重新组装数据包的方式非常相似,只是级别更高而已。 为确保正确地接收数据,TCP要求在目标计算机成功收到数据时发回一个确认(即 ACK)。如果在某个时限内未收到相应的 ACK,将重新传送数据包。如果网络拥塞,这种重新传送将导致发送的数据包重复。但是,接收计算机可使用数据包的序号来确定它是否为重复数据包,并在必要时丢弃它。 TCP与UDP的选择 如果比较UDP包和TCP包的结构,很明显UDP包不具备TCP包复杂的可靠性与控制机制。与TCP协议相同,UDP的源端口数和目的端口数也都支持一台主机上的多个应用。一个16位的UDP包包含了一个字节长的头部和数据的长度,校验码域使其可以进行整体校验。(许多应用只支持UDP,如:多媒体数据流,不产生任何额外的数据,即使知道有破坏的包也不进行重发。) 很明显,当数据传输的性能必须让位于数据传输的完整性、可控制性和可靠性时,TCP协议是当然的选择。当强调传输性能而不是传输的完整性时,如:音频和多媒体应用,UDP是最好的选择。 在数据传输时间很短,以至于此前的连接过程成为整个流量主体的情况下,UDP也是一个好的选择,如:DNS交换。把SNMP建立在UDP上的部分原因是设计者认为当发生网络阻塞时,UDP较低的开销使其有更好的机会去传送管理数据。TCP丰富的功能有时会导致不可预料的性能低下,但是我们相信在不远的将来,TCP可靠的点对点连接将会用于绝大多数的网络应用。 TCP协议和UDP协议特性区别总结: TCP协议在传送数据段的时候要给段标号;UDP协议不 TCP协议可靠;UDP协议不可靠 TCP协议是面向连接;UDP协议采用无连接 TCP协议负载较高,采用虚电路;UDP采用无连接 TCP协议的发送方要确认接收方是否收到数据段(3次握手协议) TCP协议采用窗口技术和流控制
Android使用DownloadManager实现文件下载 Android使用DownloadManager实现文件下载 下载 创建下载链接 设置允许下载的网络环境 Notification显示下载进度 设置保存路径 下载 取消下载 下载完成的监听 Code 下载 广播接收者 注册 实现 下载 创建下载链接 DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); 设置允许下载的网络环境 request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); WIFI网络 : DownloadManager.Request.NETWORK_WIFI 移动网络 : DownloadManager.Request.NETWORK_MOBILE Notification显示下载进度 // 在Notification显示下载进度 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE); // 设置Title request.setTitle("更新"); // 设置描述 request.setDescription("正在下载更新文件..."); 设置保存路径 private static final String DIR = "AutoUpdate"; private static final String APK = "MyHome.apk"; private static final String PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + DIR + "/" + APK; request.setDestinationInExternalPublicDir(DIR, APK); 下载 下载会返回一个进程ID DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); long id = downloadManager.enqueue(request); 取消下载 通过ID可以需要下载 downloadManager.remove(id); 下载完成的监听 下载完成,系统会发出广播,通过注册广播监听者可以监听到下载完成 广播的Action为DownloadManager.ACTION_DOWNLOAD_COMPLETE /** * Broadcast intent action sent by the download manager when the user clicks on a running * download, either from a system notification or from the downloads UI. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public final static String ACTION_NOTIFICATION_CLICKED = "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"; Code 下载 DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); // WIFI状态下下载 request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); // 设置通知栏 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE); request.setTitle("更新"); request.setDescription("正在下载更新文件..."); // 存放路径 request.setDestinationInExternalPublicDir(DIR, APK); // 开始下载 DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); long id = downloadManager.enqueue(request); 广播接收者 注册 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.kongqingwei.downloadmanagerdemo"> <!-- 网络权限 --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MANAGE_USERS"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <receiver android:name=".AutoUpdateBroadcastReceiver"> <intent-filter> <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/> </intent-filter> </receiver> </application> </manifest> 实现 package com.example.kongqingwei.downloadmanagerdemo; import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.widget.Toast; /** * Created by kongqingwei on 2016/12/19. * 广播接收者 */ public class AutoUpdateBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { Toast.makeText(context, "下载完成", Toast.LENGTH_SHORT).show(); boolean isInstalled = AutoUpdater.installApk(); Toast.makeText(context, isInstalled ? "安装成功" : "安装失败", Toast.LENGTH_SHORT).show(); } } }
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN Android自定义View绘图基础 Android自定义View绘图基础 View的测量 View的绘制 画笔属性 Shader 点 直线 矩形 圆角矩形 圆 扇形 弧形 椭圆 文字 绘制路径 图形裁剪 View的测量 控件的测量可以说是固定写法,原生的View只支持EXACTLY的测量模式,我们自定义的控件可以重写onMeasure方法 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getMeasuredSize(widthMeasureSpec), getMeasuredSize(heightMeasureSpec)); } onMeasure方法给我们返回的widthMeasureSpec和heightMeasureSpec,我们并不能直接拿过来使用,需要使用MeasureSpec类进行解析,来获取测量后的具体值。 首先需要获取测量模式 MeasureSpec.getMode(measureSpec) getMode方法返回是测量的模式,有以下3种类型: - MeasureSpec.EXACTLY : 精确值模式(指定值/match_parent) - MeasureSpec.AT_MOST : 最大值模式(wrap_content) - MeasureSpec.UNSPECIFIED : 不指定大小的测量模式(一般用不上) 获取到了测量模式以后,获取测量后的大小 MeasureSpec.getSize(measureSpec) 根据上面的意思,可以封装我们的getMeasuredSize方法 // 默认大小 private static final int DEFAULT_SIZE = 200; /** * 获取测量后的大小 * * @param measureSpec measureSpec * @return measuredSize */ private int getMeasuredSize(int measureSpec) { switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.EXACTLY: // 精确值模式(指定值/match_parent) Log.i(TAG, "getMeasuredSize: 精确值模式"); return MeasureSpec.getSize(measureSpec); case MeasureSpec.AT_MOST: // 最大值模式(wrap_content) Log.i(TAG, "getMeasuredSize: 最大值模式"); return Math.min(DEFAULT_SIZE, MeasureSpec.getSize(measureSpec)); case MeasureSpec.UNSPECIFIED: // 不指定大小的测量模式 return DEFAULT_SIZE; default: return DEFAULT_SIZE; } } 现在我们自定义的View就支持自定义的大小了,包括match_parent、wrap_content、具体值。 View的绘制 画笔属性 创建画笔 Paint paint = new Paint(); 方法 描述 举例 public void setAntiAlias(boolean aa) 设置画笔锯齿效果 true 无锯齿效果 public void setColor(@ColorInt int color) 设置画笔颜色 public void setARGB(int a, int r, int g, int b) 设置画笔的A、R、G、B值 public void setAlpha(int a) 设置画笔的Alpha值 public void setTextSize(float textSize) 设置字体的尺寸 public void setStyle(Style style) 设置画笔的风格(空心或实心) paint.setStyle(Paint.Style.STROKE);// 空心 paint.setStyle(Paint.Style.FILL); // 实心 public void setStrokeWidth(float width) 设置空心边框的宽度 Shader BitmapShader, ComposeShader, LinearGradient, RadialGradient, SweepGradient 点 /** * Helper for drawPoints() for drawing a single point. */ public void drawPoint(float x, float y, @NonNull Paint paint) canvas.drawPoint(10, 10, paint); 直线 绘制一条直线 /** * Draw a line segment with the specified start and stop x,y coordinates, * using the specified paint. * * <p>Note that since a line is always "framed", the Style is ignored in the paint.</p> * * <p>Degenerate lines (length is 0) will not be drawn.</p> * * @param startX The x-coordinate of the start point of the line * @param startY The y-coordinate of the start point of the line * @param paint The paint used to draw the line */ public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) 绘制多条直线 public void drawLines(@Size(multiple=4) @NonNull float[] pts, @NonNull Paint paint) 矩形 /** * Draw the specified Rect using the specified paint. The rectangle will * be filled or framed based on the Style in the paint. * * @param left The left side of the rectangle to be drawn * @param top The top side of the rectangle to be drawn * @param right The right side of the rectangle to be drawn * @param bottom The bottom side of the rectangle to be drawn * @param paint The paint used to draw the rect */ public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) canvas.drawRect(100, 100, 200, 200, paint); 圆角矩形 /** * Draw the specified round-rect using the specified paint. The roundrect * will be filled or framed based on the Style in the paint. * * @param rx The x-radius of the oval used to round the corners * @param ry The y-radius of the oval used to round the corners * @param paint The paint used to draw the roundRect */ public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint) canvas.drawRoundRect(100, 100, 200, 200, 20, 20, paint); 圆 /** * Draw the specified circle using the specified paint. If radius is <= 0, * then nothing will be drawn. The circle will be filled or framed based * on the Style in the paint. * * @param cx The x-coordinate of the center of the cirle to be drawn * @param cy The y-coordinate of the center of the cirle to be drawn * @param radius The radius of the cirle to be drawn * @param paint The paint used to draw the circle */ public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) // 画圆 canvas.drawCircle(200, 200, 100, paint); 扇形 /** * <p>Draw the specified arc, which will be scaled to fit inside the * specified oval.</p> * * <p>If the start angle is negative or >= 360, the start angle is treated * as start angle modulo 360.</p> * * <p>If the sweep angle is >= 360, then the oval is drawn * completely. Note that this differs slightly from SkPath::arcTo, which * treats the sweep angle modulo 360. If the sweep angle is negative, * the sweep angle is treated as sweep angle modulo 360</p> * * <p>The arc is drawn clockwise. An angle of 0 degrees correspond to the * geometric angle of 0 degrees (3 o'clock on a watch.)</p> * * @param startAngle Starting angle (in degrees) where the arc begins * @param sweepAngle Sweep angle (in degrees) measured clockwise * @param useCenter If true, include the center of the oval in the arc, and close it if it is being stroked. This will draw a wedge * @param paint The paint used to draw the arc */ public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) // 扇形 起始角度0度 旋转角度120度 canvas.drawArc(100, 100, 200, 200, 0, 120, true, paint); 弧形 扇形通过调整属性,可以实现弧形的效果,首先画笔设置成空心 // 空心 paint.setStyle(Paint.Style.STROKE); 设置画扇形的useCenter参数为false // 弧形 起始角度0度 旋转角度120度 canvas.drawArc(100, 100, 200, 200, 0, 120, false, paint); 椭圆 /** * Draw the specified oval using the specified paint. The oval will be * filled or framed based on the Style in the paint. */ public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) // 椭圆 canvas.drawOval(100, 100, 500, 300, paint); 文字 /** * Draw the text, with origin at (x,y), using the specified paint. The * origin is interpreted based on the Align setting in the paint. * * @param text The text to be drawn * @param x The x-coordinate of the origin of the text being drawn * @param y The y-coordinate of the baseline of the text being drawn * @param paint The paint used for the text (e.g. color, size, style) */ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) // 文字 paint.setTextSize(30); paint.setColor(Color.BLACK); canvas.drawText("KongQingwei", 100, 100, paint); 在指定位置显示每个字符 /** * Draw the text in the array, with each character's origin specified by * the pos array. * * @param text The text to be drawn * @param pos Array of [x,y] positions, used to position each character * @param paint The paint used for the text (e.g. color, size, style) * * @deprecated This method does not support glyph composition and decomposition and * should therefore not be used to render complex scripts. It also doesn't * handle supplementary characters (eg emoji). */ @Deprecated public void drawPosText(@NonNull String text, @NonNull @Size(multiple=2) float[] pos, @NonNull Paint paint) canvas.drawPosText("KongQingwei", new float[]{ 30,30, 60,60, 90,90, 120,120, 150,150, 180,180, 210,210, 240,240, 270,270, 300,300, 330,330 }, paint); 绘制路径 可以自定义画出想要的任意图形 五角星 /** * Draw the specified path using the specified paint. The path will be * filled or framed based on the Style in the paint. * * @param path The path to be drawn * @param paint The paint used to draw the path */ public void drawPath(@NonNull Path path, @NonNull Paint paint) Path path = new Path(); path.moveTo(0,100); path.lineTo(250,300); path.lineTo(150,0); path.lineTo(50,300); path.lineTo(300,100); path.lineTo(0,100); canvas.drawPath(path,paint); 空心 paint.setStyle(Paint.Style.STROKE); 图形裁剪 以上面的实心五角星为例,以五角星的中心为圆心,裁剪出一个半径为100的圆形 /** * Modify the current clip with the specified path. * * @param path The path to operate on the current clip * @param op How the clip is modified * @return true if the resulting is non-empty */ public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) // 裁剪一个圆形 Path path = new Path(); path.reset(); path.addCircle(150, 150, 100, Path.Direction.CCW); canvas.clipPath(path, Region.Op.INTERSECT); // canvas.save(); // 画五角星 path.reset(); path.moveTo(0,100); path.lineTo(250,300); path.lineTo(150,0); path.lineTo(50,300); path.lineTo(300,100); path.lineTo(0,100); canvas.drawPath(path,paint); Region.Op 描述 样式 INTERSECT 裁剪内部交集 DIFFERENCE 外部
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN 本篇参考Android与HTML+JS交互入门 效果图 加载本地Html contentWebView = (WebView) findViewById(R.id.webview); // 加载Assets下的Html contentWebView.loadUrl("file:///android_asset/html/test.html"); 启用Javascript contentWebView.getSettings().setJavaScriptEnabled(true); contentWebView.addJavascriptInterface(this, "android"); Android调用Javascript的方法 Javascript写法 <script type="text/javascript"> function jsFunction(){ document.getElementById("content").innerHTML = "JS方法被调用"; } function jsFunctionArg(arg){ document.getElementById("content").innerHTML = "JS方法被调用并收到参数:<br/>" + arg; } </script> Android写法 // 调用JS的jsFunction方法 contentWebView.loadUrl("javascript:jsFunction()"); // 调用JS的jsFunctionArg方法 contentWebView.loadUrl("javascript:jsFunctionArg('[Android传递过来的数据]')"); Javascript调用Android的方法 Android方法 @JavascriptInterface public void androidFunction() { Snackbar.make(contentWebView, "Android的方法被调用", Snackbar.LENGTH_SHORT).show(); } @JavascriptInterface public void androidFunction(String text) { Snackbar.make(contentWebView, "Android的方法被调用并收到参数 : \n" + text, Snackbar.LENGTH_SHORT).show(); } Javascript调用 <input type="button" style="width:300px;height:50px;" value="JS调用Android方法" onclick="window.android.androidFunction()" /> <input type="button" style="width:300px;height:50px;" value="JS调用Android带有参数的方法" onclick="window.android.androidFunction('[JS传递过来的数据]')" /> Code HTML <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=gb2312"> <script type="text/javascript"> function jsFunction(){ document.getElementById("content").innerHTML = "JS方法被调用"; } function jsFunctionArg(arg){ document.getElementById("content").innerHTML = "JS方法被调用并收到参数:<br/>" + arg; } </script> </head> <body> <h1>HTML页面</h1> <hr/> <h3><div id="content">|</div></h3> <hr/> <input type="button" style="width:300px;height:50px;" value="JS调用Android方法" onclick="window.android.androidFunction()" /> <br/> <input type="button" style="width:300px;height:50px;" value="JS调用Android带有参数的方法" onclick="window.android.androidFunction('[JS传递过来的数据]')" /> </body> </html> 测试类 package com.kongqw.kqwandroidjsdemo; import android.annotation.SuppressLint; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.webkit.JavascriptInterface; import android.webkit.WebView; public class MainActivity extends AppCompatActivity { private WebView contentWebView; @SuppressLint({"JavascriptInterface", "SetJavaScriptEnabled", "AddJavascriptInterface"}) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); contentWebView = (WebView) findViewById(R.id.webview); // 加载Assets下的Html contentWebView.loadUrl("file:///android_asset/html/test.html"); // 启用Javascript contentWebView.getSettings().setJavaScriptEnabled(true); contentWebView.addJavascriptInterface(this, "android"); } @JavascriptInterface public void androidFunction() { Snackbar.make(contentWebView, "Android的方法被调用", Snackbar.LENGTH_SHORT).show(); } @JavascriptInterface public void androidFunction(String text) { Snackbar.make(contentWebView, "Android的方法被调用并收到参数 : \n" + text, Snackbar.LENGTH_SHORT).show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_js_function1) { // 调用JS的jsFunction方法 contentWebView.loadUrl("javascript:jsFunction()"); return true; } else if (id == R.id.action_js_function2) { // 调用JS的jsFunctionArg方法 contentWebView.loadUrl("javascript:jsFunctionArg('[Android传递过来的数据]')"); return true; } return super.onOptionsItemSelected(item); } }
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN 有些时候程序需要播放几个很短的低延迟的音效来响应与用户的交互。 Android通过SoundPool将文件音频缓存加载到内存中,然后在响应用户操作的时候快速地播放。 Android框架低通了SoundPool来解码小音频文件,并在内存中操作它们来进行音频快速和重复的播放。SoundPool还有一些其他特性,比如可以在运行时控制音量和播放速度。 播放音效也很简单,总共分5步 准备音频文件 将音频文件放置在assets目录下 初始化SoundPool SoundPool mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); 加载音频文件 int streamID = mSoundPool.load(getApplicationContext().getAssets().openFd("beep/beep1.mp3"), 1); 播放音频文件 mSoundPool.play(streamID, 10, 10, 1, 0, 1.0f); 释放SoundPool mSoundPool.release(); mSoundPool = null; Code package com.kongqw.kqwplaybeepdemo; import android.media.AudioManager; import android.media.SoundPool; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import java.io.IOException; import java.util.HashMap; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = "MainActivity"; private SoundPool mSoundPool; private int streamID; private HashMap<String, Integer> mSoundMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_beep1).setOnClickListener(this); findViewById(R.id.button_beep2).setOnClickListener(this); mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); mSoundMap = new HashMap<>(); try { streamID = mSoundPool.load(getApplicationContext().getAssets().openFd("beep/beep1.mp3"), 1); mSoundMap.put("beep1.mp3", streamID); streamID = mSoundPool.load(getApplicationContext().getAssets().openFd("beep/beep2.mp3"), 1); mSoundMap.put("beep2.mp3", streamID); Log.i(TAG, "onCreate: streamID = " + streamID); } catch (IOException e) { e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); mSoundPool.release(); mSoundPool = null; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.button_beep1: streamID = mSoundMap.get("beep1.mp3"); mSoundPool.play(streamID, 10, 10, 1, 0, 1.0f); break; case R.id.button_beep2: streamID = mSoundMap.get("beep2.mp3"); mSoundPool.play(streamID, 10, 10, 1, 0, 1.0f); break; default: break; } } }
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN 看了医生写的文章一触即发——App启动优化最佳实践,收获是有的。 做Android开发,一定写给过启动页,在这里做一些初始化的操作,还有就是显示推广信息。 很普通的一个页面,以前测试也给我提出过bug,应用在启动的时候,有时候有白屏/黑屏。当时能做的就是尽量较少耗时操作,上面医生的文章里也有提到,但是通过主题的方式优化这个问题之前还真是不知道的。 下面主要总结一下通过主题的方式优化启动页(医生还提到了在子线程初始化和使用IntentService初始化,都是属于异步初始化,还有延迟初始化,就不说了) 效果图 通过修改主题优化启动时白屏/黑屏 原理请移步到医生的文章,我就不复述了,之所以会看到白屏或者黑屏,是和我们的主题有关系的,因为系统默认使用的主题,背景色就是白色/黑色。那么我们自定义一个主题,让默认的样式就是我们想要的,就优化了白屏/黑屏的问题。 首先,我们自定义一个主题,设置一个我们想要的背景 <!-- 启动页主题 --> <style name="SplashTheme" parent="@style/Theme.AppCompat.Light.NoActionBar"> <item name="android:windowBackground">@drawable/start_window</item> </style> 自定义背景start_window.xml <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque"> <!-- The background color, preferably the same as your normal theme --> <item android:drawable="@android:color/holo_blue_dark" /> <!-- Your product logo - 144dp color version of your app icon --> <item> <bitmap android:gravity="center" android:src="@mipmap/ic_launcher" /> </item> </layer-list> 最后,在清单文件设置启动页使用我们自定义的主题 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bitmain.launchtimedemo"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <!-- 启动页 --> <activity android:name=".SplashActivity" android:theme="@style/SplashTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 主页 --> <activity android:name=".MainActivity" /> </application> </manifest> 到此大功告成,为了体现出效果,在启动页加载之前,我们模拟一个白屏/黑屏的延时操作 public class SplashActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 模拟系统初始化 白屏、黑屏 SystemClock.sleep(1000); setContentView(R.layout.activity_splash); // 启动后 停留2秒进入到主页面 new Handler().postDelayed(new Runnable() { @Override public void run() { Intent intent = new Intent(SplashActivity.this, MainActivity.class); startActivity(intent); finish(); } }, 2000); } }
在广播中弹出对话框与在Activiity中弹出对话框有所不同,在Activiity中弹出对话框,我们需要用到当前Activiity的Context,而在广播中并没有,如果一定要在广播中弹出一个对话框,我需要定义这个对话框是一个系统级别的。 首先需要有弹出系统对话框的权限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> 在show()之前,要先设置Dialog的类型为TYPE_SYSTEM_ALERT。 alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); 最后,最最重要的,AlertDialog要使用android.app.AlertDialog,不要用v7包下的Dialog,会报错。 由于是系统级别的Dialog,是需要悬浮窗权限的,类似小米手机,默认就是关闭了悬浮窗权限,所以执行了代码也没有任何效果,需要手动把权限加上才能正常弹出,所以一般也建议这样做。 Code package com.example.kongqw.dialoginbroadcastreceiverdemo; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.view.WindowManager; /** * Created by kqw on 2016/11/3. * MyBroadcastReceiver */ public class MyBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // 获得广播发送的数据 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); dialogBuilder.setTitle("提示"); dialogBuilder.setMessage("这是在BroadcastReceiver弹出的对话框。"); dialogBuilder.setCancelable(false); dialogBuilder.setPositiveButton("确定", null); AlertDialog alertDialog = dialogBuilder.create(); alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); alertDialog.show(); } }
Android串口通信 串口通信偏向嵌入式一点,是Android设备通过串口与其他设备进行通信的一种方式,本文介绍的Android纯串口的通信,并不是手机上的USB串口通信。 手机上是没有这个串口的哦。 关于串口通信,Google已经给出了源码,地址在GitHub android-serialport-api 四年前的代码,还是Eclipse工程,本文主要介绍如何在Android Studio中使用。 源码地址在 KqwSerialPortDemo 集成 Java层的代码,Googley已经给封装好了,主要的都在 SerialPort.java 1.导入so 没有什么难度了,将so导入到项目 2.导入jni文件 在main目录下创建cpp文件夹,并将jni源文件和CMakeLists.txt导入 在build.gradle修改cmake路径。 android { …… externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } } …… } 3. 修改jni源文件 这里要注意jni文件函数名的写法:Java_包名_类名_方法名 在将源码里的jni导入过来的时候,包名是源码Demo的包名,我们在自己的工程里要换成自己的包名、类名,源文件和头文件都要记得改。 4. 修改CMakeLists.txt与SerialPort.java CMakeLists.txt cmake_minimum_required(VERSION 3.4.1) add_library(SerialPort SHARED SerialPort.c) # Include libraries needed for libserial_port lib target_link_libraries(SerialPort android log) SerialPort.java static { System.loadLibrary("SerialPort"); System.loadLibrary("serial_port"); } 使用 基类 需要使用串口通信的类继承 SerialPortActivity.java 打开串口 端口号:/dev/ttyS2 比特率:115200 public SerialPort getSerialPort() throws SecurityException, IOException, InvalidParameterException { if (mSerialPort == null) { mSerialPort = new SerialPort(new File("/dev/ttyS2"), 115200, 0); } return mSerialPort; } 关闭串口 public void closeSerialPort() { if (mSerialPort != null) { mSerialPort.close(); mSerialPort = null; } } 发送数据 Message message = Message.obtain(); message.obj = text.getBytes(); sendingHandler.sendMessage(message); 接收消息 @Override protected void onDataReceived(final byte[] buffer, final int size) { runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(mApplication, "收到消息:" + new String(buffer) + " size = " + size, Toast.LENGTH_SHORT).show(); } }); } 下载并安装NDK与CMake
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN OpenCV主要实现人脸检测功能 JavaCV主要实现人脸对比功能 具体的就不啰嗦了,本来最近很忙,主要是因为好多人私信我要 Android使用OpenCV实现「人脸检测」和「人脸识别」 的Demo,今天特意抽出时间写了一下。 效果图 源码 KqwFaceDetectionDemo 感觉有用的话,就给个star吧,谢谢!! 注意 最后啰嗦一点,如果你的程序是跑在手机、pad等设备上,一般没有什么问题。 但是如果你是在自己的开发板上跑,可能会有一些小插曲。 比如我司的机器人是定制的Android板子,对系统做了裁剪,很多摄像头的方法可能就用不了 例如这样一个错误 AndroidRuntime: java.lang.RuntimeException: setParameters failed 当打开程序的时候,OpenCV会提示,没有找到可用摄像头或者摄像头被锁住(大概这个意思,我就不截图了),一种可能是设备真的没有接摄像头,也有可能是摄像头定制过,导致某些方法用不了,比如上面的错误就是我遇到的其中一个。
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN 部署 To get a Git project into your build: Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end of repositories: allprojects { repositories { ... maven { url 'https://jitpack.io' } } } Step 2. Add the dependency dependencies { compile 'com.github.kongqw:AndroidRocker:1.0.1' } 使用 XML <com.kongqw.rockerlibrary.view.RockerView android:id="@+id/rockerView" android:layout_width="200dp" android:layout_height="200dp" kongqw:areaBackground="@drawable/default_area_bg" kongqw:rockerBackground="@drawable/default_rocker_bg" kongqw:rockerRadius="30dp" /> Activity 初始化 RockerView rockerView = (RockerView) findViewById(R.id.rockerView); 设置回调模式 // 设置回调模式 rockerView.setCallBackMode(RockerView.CallBackMode.CALL_BACK_MODE_STATE_CHANGE); 监听摇动方向 // 监听摇动方向 rockerView.setOnShakeListener(RockerView.DirectionMode.DIRECTION_8, new RockerView.OnShakeListener() { @Override public void onStart() { mLogLeft.setText(null); } @Override public void direction(RockerView.Direction direction) { mLogLeft.setText("摇动方向 : " + getDirection(direction)); } @Override public void onFinish() { mLogLeft.setText(null); } }); 监听摇动角度 // 监听摇动角度 rockerViewRight.setOnAngleChangeListener(new RockerView.OnAngleChangeListener() { @Override public void onStart() { mLogRight.setText(null); } @Override public void angle(double angle) { mLogRight.setText("摇动角度 : " + angle); } @Override public void onFinish() { mLogRight.setText(null); } }); 效果图 源码 KqwRockerDemo 喜欢就给个star,谢谢! 功能 支持自适应大小 支持2个方向、4个方向、8个方向的摇动监听 支持摇动角度获取 可选回调模式 支持可摇动区域自定义 支持摇杆自定义 支持设置图片、色值、Shape图形 使用 <kong.qingwei.rockerlibrary.RockerView android:id="@+id/rockerView_center" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerHorizontal="true" kongqw:areaBackground="#FF333333" kongqw:rockerBackground="#FF987654" kongqw:rockerRadius="15dp" /> 参数 参数 是否必须 描述 areaBackground 可选 可摇动区域的背景 rockerBackground 可选 摇杆的背景 rockerRadius 可选 摇杆半径 设置回调方式 setCallBackMode(CallBackMode mode) 参数 回调方式 描述 CALL_BACK_MODE_MOVE 有移动就立刻回调 CALL_BACK_MODE_STATE_CHANGE 状态有变化的时候回调 监听摇动角度 返回角度的取值范围:[0°,360°) setOnAngleChangeListener(OnAngleChangeListener listener) 监听摇动方向 setOnShakeListener(DirectionMode directionMode, OnShakeListener listener) 支持监听的方向 方向 图 描述 DIRECTION_2_HORIZONTAL 横向 左右两个方向 DIRECTION_2_VERTICAL 纵向 上下两个方向 DIRECTION_4_ROTATE_0 四个方向 DIRECTION_4_ROTATE_45 四个方向 旋转45° DIRECTION_8 八个方向 方向描述 方向 描述 DIRECTION_LEFT 左 DIRECTION_RIGHT 右 DIRECTION_UP 上 DIRECTION_DOWN 下 DIRECTION_UP_LEFT 左上 DIRECTION_UP_RIGHT 右上 DIRECTION_DOWN_LEFT 左下 DIRECTION_DOWN_RIGHT 右下 DIRECTION_CENTER 中间 例子 RockerView rockerViewLeft = (RockerView) findViewById(R.id.rockerView_left); if (rockerViewLeft != null) { rockerViewLeft.setCallBackMode(RockerView.CallBackMode.CALL_BACK_MODE_STATE_CHANGE); rockerViewLeft.setOnShakeListener(RockerView.DirectionMode.DIRECTION_8, new RockerView.OnShakeListener() { @Override public void onStart() { mLogLeft.setText(null); } @Override public void direction(RockerView.Direction direction) { mLogLeft.setText("摇动方向 : " + getDirection(direction)); } @Override public void onFinish() { mLogLeft.setText(null); } }); } RockerView rockerViewRight = (RockerView) findViewById(R.id.rockerView_right); if (rockerViewRight != null) { rockerViewRight.setOnAngleChangeListener(new RockerView.OnAngleChangeListener() { @Override public void onStart() { mLogRight.setText(null); } @Override public void angle(double angle) { mLogRight.setText("摇动角度 : " + angle); } @Override public void onFinish() { mLogRight.setText(null); } }); }
转载请说明出处! 作者:kqw攻城狮 出处:个人站 | CSDN 需求:左右声道分别输出不同的音频数据,波形要是一个正弦波,左右声道还要对称! 对硬件不是很了解,说是要通过音波避障。 效果图 之前已经介绍了如何在左右声道输出不同的音频数据。 那么这里主要介绍如何模拟出波形是正弦波的音频数据。 模拟正弦波 /** * 模拟正弦波音频数据 * @param isLeft 左右声道 * @return 音频数据 */ private short[] initData(boolean isLeft) { double phase = 0.0; int amp = 10000; short[] data = new short[bufferSize]; double phaseIncrement = (2 * Math.PI * mFrequency) / mSampleRateInHz; for (int i = 0; i < bufferSize; i++) { if (isLeft) { data[i] = (short) (amp * Math.sin(phase)); } else { data[i] = (short) (-amp * Math.sin(phase)); } phase += phaseIncrement; Log.i(TAG, "initData: isLeft = " + isLeft + " buffer[" + i + "] = " + data[i]); } return data; } 主要参数 mFrequency:频率 mSampleRateInHz:采样率 // 单声道 private int mChannelConfig = AudioFormat.CHANNEL_OUT_MONO; // 频率 private int mFrequency = 19000; // 采样率 private int mSampleRateInHz = 44100; 播放音频的线程封装 package kong.qingwei.myapplication; import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.os.Build; import android.util.Log; /** * Created by kqw on 2016/8/29. * 播放音乐的线程 */ public class ChannelThread extends Thread { private static final String TAG = "ChannelThread"; private AudioTrack mAudioTrack; private short[] mData; /** * 构造方法 * * @param channelConfig 声道 * @param sampleRateInHz 采样率 * @param data 音频数据 * @param bufferSize 缓存大小 * @param isLeft 左右声道 */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ChannelThread(int channelConfig, int sampleRateInHz, short[] data, int bufferSize, boolean isLeft) { mData = data; mAudioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); if (isLeft) { mAudioTrack.setStereoVolume(AudioTrack.getMaxVolume(), 0); } else { mAudioTrack.setStereoVolume(0, AudioTrack.getMaxVolume()); } } @Override public void run() { super.run(); try { if (null != mAudioTrack) { mAudioTrack.play(); while (AudioTrack.PLAYSTATE_STOPPED != mAudioTrack.getPlayState()) { mAudioTrack.write(mData, 0, mData.length); } } Log.i(TAG, "run: End"); } catch (Exception e) { e.printStackTrace(); } } /** * 释放AudioTrack */ public void releaseAudioTrack() { if (null != mAudioTrack) { mAudioTrack.stop(); mAudioTrack.release(); mAudioTrack = null; } } } 播放 mLeftChannelThread = new ChannelThread(mChannelConfig, mSampleRateInHz, mDataLeft, bufferSize, true); mRightChannelThread = new ChannelThread(mChannelConfig, mSampleRateInHz, mDataRight, bufferSize, false); mLeftChannelThread.start(); mRightChannelThread.start(); 停止 if (null != mLeftChannelThread) { mLeftChannelThread.releaseAudioTrack(); mLeftChannelThread = null; } if (null != mRightChannelThread) { mRightChannelThread.releaseAudioTrack(); mRightChannelThread = null; } 不足 这里介绍的是在程序中模拟出一个波形满足正弦波的音频数据,还有一种方式,可以事先准备好一个这样的音频文件,直接播放就可以了。 在程序中模拟音频数据有一个缺点,就是不能保证两个线程完完全全的同步,即便是同时开启两个线程也有一先一后,在频率很高的时候,难免会有一点误差!像下面这样: 另外,这个波形和硬件有很大关系,越是低配设备,误差可能会越大,相同的趋势,但是波动的幅度会比较大(线很粗),可能和设备本身的噪音有关系。
效果图 源码 源码下载,请先移步Android左右声道的控制 我这里主要是用到了AudioTrack实现的左右声道的控制,手机一般都只有两个声道,即左声道和右声道,我们在输出的时候可以选择单声道,也可以选择双声道(立体声)。 查看了AudioTrack的API,提供了play()、pause()、stop()、write()等一系列的方法。 通过write()方法,可以实现将音频数据发送出去(播放出来)。 AudioTrack对象的构造 有三个构造方法 AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode, int sessionId) AudioTrack (AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId) 主要参数有如下几个 streamType:以什么形式播放 STREAM_VOICE_CALL STREAM_SYSTEM STREAM_RING STREAM_MUSIC STREAM_ALARM STREAM_NOTIFICATION sampleRateInHz:采样率 channelConfig:声道 AudioFormat.CHANNEL_OUT_MONO:输出单声道音频数据 AudioFormat.CHANNEL_OUT_STEREO:输出双声道音频数据(立体声) audioFormat:音频数据格式 mode:缓冲模式 MODE_STATIC:一次性将音频载入以后再播放 MODE_STREAM:以流的形式,加载一点就播放一点 把channelConfig的相关参数都看了一遍,没发现有可以指定向某声道发送数据的,只能通过AudioFormat.CHANNEL_OUT_MONO和AudioFormat.CHANNEL_OUT_STEREO选择是输出单声道的音频数据还是双声道的音频数据。 左右声道控制 构造的时候不能选择指定声道输出音频,但是有这样一个方法 setStereoVolume(float leftGain, float rightGain) 可以通过把某一个声道的音量设置到最小,达到只想某个声道输出音频的效果。 我自己也有点”呵呵“,但是也没有发现还有别的方法可以实现这样的效果。 这个方法还有一点小问题,在个别手机上,即使将某个声道的声音设置到了最小,也还是会有一点声音,这个我也还没有搞清楚为什么,个人猜测可能和手机硬件有关系。 封装 我这里的缓冲模式使用的MODE_STREAM的形式,以流的形式播放,因为这个逻辑要稍微复杂一点,尤其是暂停以后再继续播放的位置。 package kong.qingwei.androidsoundmanagerdemo; import android.app.Activity; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; /** * Created by kqw on 2016/8/26. * 播放音乐的线程 */ public class PlayThread extends Thread { // 采样率 private int mSampleRateInHz = 16000; // 单声道 private int mChannelConfig = AudioFormat.CHANNEL_OUT_MONO; // 双声道(立体声) // private int mChannelConfig = AudioFormat.CHANNEL_OUT_STEREO; private static final String TAG = "PlayThread"; private Activity mActivity; private AudioTrack mAudioTrack; private byte[] data; private String mFileName; public PlayThread(Activity activity, String fileName) { mActivity = activity; mFileName = fileName; int bufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz, mChannelConfig, AudioFormat.ENCODING_PCM_16BIT); mAudioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, mSampleRateInHz, mChannelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); } @Override public void run() { super.run(); try { if (null != mAudioTrack) mAudioTrack.play(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); InputStream inputStream = mActivity.getResources().getAssets().open(mFileName); // 缓冲区 byte[] buffer = new byte[1024]; // 播放进度 int playIndex = 0; // 是否缓冲完成 boolean isLoaded = false; // 缓冲 + 播放 while (null != mAudioTrack && AudioTrack.PLAYSTATE_STOPPED != mAudioTrack.getPlayState()) { // 字符长度 int len; if (-1 != (len = inputStream.read(buffer))) { byteArrayOutputStream.write(buffer, 0, len); data = byteArrayOutputStream.toByteArray(); Log.i(TAG, "run: 已缓冲 : " + data.length); } else { // 缓冲完成 isLoaded = true; } if (AudioTrack.PLAYSTATE_PAUSED == mAudioTrack.getPlayState()) { // TODO 已经暂停 } if (AudioTrack.PLAYSTATE_PLAYING == mAudioTrack.getPlayState()) { Log.i(TAG, "run: 开始从 " + playIndex + " 播放"); playIndex += mAudioTrack.write(data, playIndex, data.length - playIndex); Log.i(TAG, "run: 播放到了 : " + playIndex); if (isLoaded && playIndex == data.length) { Log.i(TAG, "run: 播放完了"); mAudioTrack.stop(); } if (playIndex < 0) { Log.i(TAG, "run: 播放出错"); mAudioTrack.stop(); break; } } } Log.i(TAG, "run: play end"); } catch (IOException e) { e.printStackTrace(); } } /** * 设置左右声道平衡 * * @param max 最大值 * @param balance 当前值 */ public void setBalance(int max, int balance) { float b = (float) balance / (float) max; Log.i(TAG, "setBalance: b = " + b); if (null != mAudioTrack) mAudioTrack.setStereoVolume(1 - b, b); } /** * 设置左右声道是否可用 * * @param left 左声道 * @param right 右声道 */ public void setChannel(boolean left, boolean right) { if (null != mAudioTrack) { mAudioTrack.setStereoVolume(left ? 1 : 0, right ? 1 : 0); mAudioTrack.play(); } } public void pause() { if (null != mAudioTrack) mAudioTrack.pause(); } public void play() { if (null != mAudioTrack) mAudioTrack.play(); } public void stopp() { releaseAudioTrack(); } private void releaseAudioTrack() { if (null != mAudioTrack) { mAudioTrack.stop(); mAudioTrack.release(); mAudioTrack = null; } } } 使用 从头开始播放 mPlayThread = new PlayThread(this, "tts1.pcm"); mPlayThread.start(); 暂停 mPlayThread.pause(); 暂停后继续播放 mPlayThread.play(); 停止播放 mPlayThread.stopp(); mPlayThread = null; 左右声道控制 // 禁用左声道(右声道同理) mPlayThread.setChannel(false, true); 向左右声道单独输出不同的音频数据 也是一个很”呵呵“的做法,但是依然还没有找到更好的方法。 构造两个AudioTrack对象,分别输出两个音频,一个禁用左声道,一个禁用右声道,达到预期效果。 mChannelLeftPlayer = new PlayThread(this, "tts1.pcm"); mChannelRightPlayer = new PlayThread(this, "tts2.pcm"); mChannelLeftPlayer.setChannel(true, false); mChannelRightPlayer.setChannel(false, true); mChannelLeftPlayer.start(); mChannelRightPlayer.start();
官网 环信 下载SDK 创建一个工程 如果想要封装性好一点,也可以在工程里再创建一个环信的Library,然后将SDK里的jar和.so,都导入到工程 清单文件 下面是环信Library库里面的清单文件,包含了权限的设置、APPKEY的设置和服务的声明等。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="……"> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.GET_TASKS" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.USE_CREDENTIALS" /> <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="android.permission.BROADCAST_STICKY" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <application android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true"> <!-- 设置环信应用的AppKey --> <meta-data android:name="EASEMOB_APPKEY" android:value="你的APPKEY" /> <!-- 声明SDK所需的service SDK核心功能--> <service android:name="com.hyphenate.chat.EMChatService" android:exported="true" /> <!-- 声明SDK所需的receiver --> <receiver android:name="com.hyphenate.chat.EMMonitorReceiver"> <intent-filter> <action android:name="android.intent.action.PACKAGE_REMOVED" /> <data android:scheme="package" /> </intent-filter> <!-- 可选filter --> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.USER_PRESENT" /> </intent-filter> </receiver> </application> </manifest> 初始化 创建一个InitApplication类,继承Application,用来初始化应用的一些信息。 在主工程的清单文件下声明 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="……"> <application android:name=".InitApplication" ……> …… </application> </manifest> 初始化 package ……; import android.app.Application; import android.widget.Toast; import com.hyphenate.chat.EMClient; import com.hyphenate.chat.EMOptions; /** * Created by kqw on 2016/8/22. * 初始化应用 */ public class InitApplication extends Application { @Override public void onCreate() { super.onCreate(); EMOptions options = new EMOptions(); // 默认添加好友时,是不需要验证的,改成需要验证 options.setAcceptInvitationAlways(false); // 初始化 EMClient.getInstance().init(this, options); // 在做打包混淆时,关闭debug模式,避免消耗不必要的资源 EMClient.getInstance().setDebugMode(true); Toast.makeText(this, "已经初始化", Toast.LENGTH_SHORT).show(); } } 部署完成
轮廓只不过是图像中连接的曲线,或者图像中连通部分的边界,轮廓通常以图像中的边缘来计算,但是,边缘和轮廓的区别在于轮廓是闭合的,而边缘可以是任意的。边缘的概念局限于点及其邻域像素,轮廓将目标作为整体进行处理。 效果图 源码 KqwOpenCVFeaturesDemo 步骤 将图像置灰 使用Canny边缘检测检测出图像的边缘 调用Imgproc.findContours()方法检测图像轮廓 在新的图像上画出轮廓 封装 OpenCV检测图像轮廓 使用 OpenCV检测图像轮廓
霍夫变换是一种被广泛使用的利用数学等式的参数形式在图像中检测形状的技术。 例如直线、椭圆、圆等形状。 霍夫变换可以检测任何能以参数形式表示的形状,随着形状的复杂(维数的增加,例如球体),计算的消耗也会增加。 我们通常考虑简单的霍夫形状,例如直线和圆。 霍夫直线 效果图 步骤 将图像置灰 调用Imgproc.HoughLinesP(cannyEdges, lines, 1, Math.PI / 180, 50, 20, 20) 方法获取直线的数据 第一个参数:图像输入 第二个参数:图像输出 第三个参数:图像指定像素中r的解析度 第四个参数:图像指定像素中θ的解析度 第五个参数:直线上点数的阈值 第六个参数:直线上点数的最小值 在图像上绘制直线 封装 OpenCV使用霍夫变换检测图像中的形状 使用 OpenCV使用霍夫变换检测图像中的形状 霍夫圆 效果图 步骤 霍夫圆与霍夫直线类似,只是等式改变了,调用 Imgproc.HoughCircles(cannyEdges, circles, Imgproc.CV_HOUGH_GRADIENT, 1, cannyEdges.rows() / 15); 获取圆的数据 封装 OpenCV使用霍夫变换检测图像中的形状 使用 OpenCV使用霍夫变换检测图像中的形状
纯粹阅读,请移步OpenCV使用Harris算法实现角点检测 效果图 源码 KqwOpenCVFeaturesDemo 角点是两条边缘的交点或者在局部邻域中有多个显著边缘方向的点。Harris角点检测是一种在角点检测中最常见的技术。 Harris角点检测器在图像上使用滑动窗口计算亮度的变化。 封装 这里用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 /** * Harris角点检测 * * @param bitmap 要检测的图片 */ public void harris(Bitmap bitmap) { if (null != mSubscriber) Observable .just(bitmap) // 检测边缘 .map(new Func1<Bitmap, Mat>() { @Override public Mat call(Bitmap bitmap) { Mat grayMat = new Mat(); Mat cannyEdges = new Mat(); // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); // Canny边缘检测器检测图像边缘 Imgproc.Canny(grayMat, cannyEdges, 10, 100); return cannyEdges; } }) // Harris对角检测 .map(new Func1<Mat, Bitmap>() { @Override public Bitmap call(Mat cannyEdges) { Mat corners = new Mat(); Mat tempDst = new Mat(); // 找出角点 Imgproc.cornerHarris(cannyEdges, tempDst, 2, 3, 0.04); // 归一化Harris角点的输出 Mat tempDstNorm = new Mat(); Core.normalize(tempDst, tempDstNorm, 0, 255, Core.NORM_MINMAX); Core.convertScaleAbs(tempDstNorm, corners); // 在新的图像上绘制角点 Random r = new Random(); for (int i = 0; i < tempDstNorm.cols(); i++) { for (int j = 0; j < tempDstNorm.rows(); j++) { double[] value = tempDstNorm.get(j, i); if (value[0] > 150) { Core.circle(corners, new Point(i, j), 5, new Scalar(r.nextInt(255), 2)); } } } // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(corners.cols(), corners.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(corners, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 使用 // 图片特征提取的工具类 mFeaturesUtil = new FeaturesUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mImageView.setImageBitmap(bitmap); } }); // Harris角点检测 mFeaturesUtil.harris(mSelectImage);
纯粹阅读,请移步OpenCV使用Sobel滤波器实现图像边缘检测 效果图 源码 KqwOpenCVFeaturesDemo Sobel滤波器也叫Sobel算子,与Canny边缘检测一样,需要计算像素的灰度梯度,只不过是换用另一种方式。 使用Sobel算子计算边缘的步骤 将图像转为灰度图像 // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); 计算水平方向灰度梯度的绝对值 Imgproc.Sobel(grayMat, grad_x, CvType.CV_16S, 1, 0, 3, 1, 0); Core.convertScaleAbs(grad_x, abs_grad_x); 计算垂直方法灰度梯度的绝对值 Imgproc.Sobel(grayMat, grad_y, CvType.CV_16S, 0, 1, 3, 1, 0); Core.convertScaleAbs(grad_y, abs_grad_y); 计算最终梯度 // 计算结果梯度 Core.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 1, sobel); 最终的梯度实质上就是边缘。 这里用到了两个3 * 3的核对图像做卷积来近似地计算水平和垂直方向的灰度梯度 封装 这里用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 /** * Sobel滤波器 * * @param bitmap 要检测的图片 */ public void sobel(Bitmap bitmap) { if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { Mat grayMat = new Mat(); Mat sobel = new Mat(); Mat grad_x = new Mat(); Mat grad_y = new Mat(); Mat abs_grad_x = new Mat(); Mat abs_grad_y = new Mat(); // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); // 计算水平方向梯度 Imgproc.Sobel(grayMat, grad_x, CvType.CV_16S, 1, 0, 3, 1, 0); // 计算垂直方向梯度 Imgproc.Sobel(grayMat, grad_y, CvType.CV_16S, 0, 1, 3, 1, 0); // 计算两个方向上的梯度的绝对值 Core.convertScaleAbs(grad_x, abs_grad_x); Core.convertScaleAbs(grad_y, abs_grad_y); // 计算结果梯度 Core.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 1, sobel); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(sobel.cols(), sobel.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(sobel, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 使用 // 图片特征提取的工具类 mFeaturesUtil = new FeaturesUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mImageView.setImageBitmap(bitmap); } }); // Sobel滤波器检测图像边缘 mFeaturesUtil.sobel(mSelectImage);
纯粹阅读,请移步OpenCV使用Canny边缘检测器实现图像边缘检测 效果图 源码 KqwOpenCVFeaturesDemo Canny边缘检测器是一种被广泛使用的算法,并被认为是边缘检测最优的算法,该方法使用了比高斯差分算法更复杂的技巧,如多向灰度梯度和滞后阈值化。 Canny边缘检测器算法基本步骤 平滑图像:通过使用合适的模糊半径执行高斯模糊来减少图像内的噪声。 计算图像的梯度:这里计算图像的梯度,并将梯度分类为垂直、水平和斜对角。这一步的输出用于在下一步中计算真正的边缘。 非最大值抑制:利用上一步计算出来的梯度方向,检测某一像素在梯度的正方向和负方向上是否是局部最大值,如果是,则抑制该像素(像素不属于边缘)。这是一种边缘细化技术,用最急剧的变换选出边缘点。 用滞后阈值化选择边缘:最后一步,检查某一条边缘是否明显到足以作为最终输出,最后去除所有不明显的边缘。 算法比较复杂,但是使用很简单,首先将图像灰度化 // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); 然后调用Imgproc.Canny()方法即可 // Canny边缘检测器检测图像边缘 Imgproc.Canny(grayMat, cannyEdges, 10, 100); 第一个参数表示图像输入 第二个参数表述图像输出 第三个参数表示低阈值 第四个参数表示高阈值 在Canny边缘检测算法中,将图像中的点归为三类: 被抑制点 灰度梯度值 < 低阈值 弱边缘点 低阈值 <= 灰度梯度值 <= 高阈值 强边缘点 高阈值 < 灰度梯度值 封装 /** * Canny边缘检测算法 * * @param bitmap 要检测的图片 */ public void canny(Bitmap bitmap) { if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { Mat grayMat = new Mat(); Mat cannyEdges = new Mat(); // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); // Canny边缘检测器检测图像边缘 Imgproc.Canny(grayMat, cannyEdges, 10, 100); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(cannyEdges.cols(), cannyEdges.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(cannyEdges, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 使用 // 图片特征提取的工具类 mFeaturesUtil = new FeaturesUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mImageView.setImageBitmap(bitmap); } }); // Canny边缘检测器检测图像边缘 mFeaturesUtil.canny(mSelectImage);
纯粹阅读,请移步OpenCV高斯差分技术实现图像边缘检测 效果图 源码 KqwOpenCVFeaturesDemo 边缘是图像中像素亮度变化明显的点。 高斯差分算法步骤 将图像转为灰度图像 // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); 用两个不同的模糊半径对灰度图像执行高斯模糊(取得两幅高斯模糊图像) // 以两个不同的模糊半径对图像做模糊处理 Imgproc.GaussianBlur(grayMat, blur1, new Size(15, 15), 5); Imgproc.GaussianBlur(grayMat, blur2, new Size(21, 21), 5); 将两幅高斯模糊图像做减法,得到一幅包含边缘点的结果图像 // 将两幅模糊后的图像相减 Mat diff = new Mat(); Core.absdiff(blur1, blur2, diff); 该方法只对图像做了高斯模糊,这是计算图像边缘最快的方法之一,但是,该方法的结果也不是很理想,这种方式对某些图像效果很好,但是在某些情况下可能会完全失效。 封装 这里用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 /** * 高斯差分算法边缘检测 * * @param bitmap 要检测的图片 */ public void differenceOfGaussian(Bitmap bitmap) { if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { Mat grayMat = new Mat(); Mat blur1 = new Mat(); Mat blur2 = new Mat(); // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 原图置灰 Imgproc.cvtColor(src, grayMat, Imgproc.COLOR_BGR2GRAY); // 以两个不同的模糊半径对图像做模糊处理 Imgproc.GaussianBlur(grayMat, blur1, new Size(15, 15), 5); Imgproc.GaussianBlur(grayMat, blur2, new Size(21, 21), 5); // 将两幅模糊后的图像相减 Mat diff = new Mat(); Core.absdiff(blur1, blur2, diff); // 反转二值阈值化 Core.multiply(diff, new Scalar(100), diff); Imgproc.threshold(diff, diff, 50, 255, Imgproc.THRESH_BINARY_INV); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(grayMat.cols(), grayMat.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(diff, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 使用 // 图片特征提取的工具类 mFeaturesUtil = new FeaturesUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mImageView.setImageBitmap(bitmap); } }); // 高斯差分技术检测图像边缘 mFeaturesUtil.differenceOfGaussian(mSelectImage);
纯粹阅读,请移步OpenCV实现图像阈值化 效果图 源码 KqwOpenCVBlurDemo 阈值化是一种将我们想要在图像中分析的区域分割出来的方法。 我们把每个像素值都与一个预设的阈值做比较,再根据比较的结果调整像素值。 类似这样 Imgproc.threshold(src,src,100,255,Imgproc.THRESH_BINARY); 其中100是阈值,255是最大值(纯白色的值)。 常量 名称 常量 二值阈值化 Imgproc.THRESH_BINARY 阈值化到零 Imgproc.THRESH_TOZERO 截断阈值化 Imgproc.THRESH_TRUNC 反转二值阈值化 Imgproc.THRESH_BINARY_INV 反转阈值化到零 Imgproc.THRESH_TOZERO_INV 自适应阈值 上述的阈值化是全局性的,我们也可以根据邻域像素为任意像素计算阈值。 自适应阈值用到的3个参数 自适应方法 Imgproc.ADAPTIVE_THRESH_MEAN_C:阈值是邻域像素的值 Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是领域像素的加权和,权重来自高斯核 块尺寸 邻域的大小 常量C 从对每个像素计算得到的均值或加权均值减去的常量 图像置灰 Imgproc.cvtColor(src, src, Imgproc.COLOR_BGR2GRAY); 自适应阈值化 Imgproc.adaptiveThreshold(src, src, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 3, 0); 封装 这里我用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 /** * 自适应阈值 * * @param bitmap 要处理的图片 */ public void adaptiveThreshold(Bitmap bitmap) { // 使用RxJava处理图片 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 图像置灰 Imgproc.cvtColor(src, src, Imgproc.COLOR_BGR2GRAY); // 自适应阈值化 Imgproc.adaptiveThreshold(src, src, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 3, 0); // 二值阈值化 // Imgproc.threshold(src,src,100,255,Imgproc.THRESH_BINARY); // 阈值化到零 // Imgproc.threshold(src,src,100,255,Imgproc.THRESH_TOZERO); // 截断阈值化 // Imgproc.threshold(src,src,100,255,Imgproc.THRESH_TRUNC); // 反转二值阈值化 // Imgproc.threshold(src,src,100,255,Imgproc.THRESH_BINARY_INV); // 反转阈值化到零 // Imgproc.threshold(src,src,100,255,Imgproc.THRESH_TOZERO_INV); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 调用 // 图片处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); // 自适应阈值 mBlurUtil.adaptiveThreshold(mSelectImage);
纯粹阅读,请移步OpenCV实现图像暗区扩张(腐蚀图片) 效果图 源码 KqwOpenCVBlurDemo 暗区扩张,也叫腐蚀,要实现这样的效果,我们可以选取一个合适大小的核,用被核覆盖的最小值代替锚点像素。 我们首先定义一个合适大小的核 Mat kernelErode = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(5, 5)); 然后调用Imgproc.erode()方法把图像的暗区放大 // 扩大暗区(腐蚀) Imgproc.erode(src, src, kernelErode); 封装 /** * 扩大图片暗区(腐蚀图片) * * @param bitmap 要处理的图片 */ public void erode(Bitmap bitmap) { // 使用RxJava处理图片 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 定义一个合适大小的核 Mat kernelErode = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(5, 5)); // 扩大暗区(腐蚀) Imgproc.erode(src, src, kernelErode); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 调用 // 图片处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); // 扩大图片暗区 mBlurUtil.erode(mSelectImage);
纯粹阅读,移步OpenCV实现图像亮区扩张 效果图 源码 KqwOpenCVBlurDemo 亮区扩张,也叫膨胀,要实现这样的效果,我们可以选取一个合适大小的核,用被核覆盖的最大值代替锚点像素。膨胀可以用来融合可能被分割的目标。 我们首先定义一个合适大小的核 Mat kernelDilate = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); 然后调用Imgproc.dilate()方法把图像的亮区放大 // 扩大亮区 Imgproc.dilate(src, src, kernelDilate); 封装 这里我用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 /** * 扩大图片亮区 * * @param bitmap 要处理的图片 */ public void dilate(Bitmap bitmap) { // 使用RxJava处理图片 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 定义一个合适大小的核 Mat kernelDilate = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); // 扩大亮区 Imgproc.dilate(src, src, kernelDilate); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 调用 // 图片处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); // 扩大图片亮区 mBlurUtil.dilate(mSelectImage);
纯粹阅读,请移步OpenCV实现图片锐化 效果图 源码 KqwOpenCVBlurDemo 锐化也可以看作是一种线性滤波操作,并且锚点像素有较高的权重,而周围的像素权重较低。 因此,我们可以自定义一个这样的核。 /* 自定义核 0 -1 0 -1 5 -1 0 -1 0 */ Mat kernel = new Mat(3, 3, CvType.CV_16SC1); kernel.put(0, 0, 0, -1, 0, -1, 5, -1, 0, -1, 0); 这里我们将图像的深度设为16SC1,表示包含一个通道(C1),图像中的每个像素包含一个16比特有符号整型数(16S)。 定义完核以后,我们对图像和核做卷积操作 // 对图像和自定义核做卷积 Imgproc.filter2D(src, src, src.depth(), kernel); 第一个参数表示输入的图像 第二个参数表示输出的图像 第三个参数表示图像的深度 第四个参数是我们自定义的核 封装 这里我用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 /** * 锐化图片 * * @param bitmap 要处理的图片 */ public void filter2D(Bitmap bitmap) { // 使用RxJava处理图片 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); /* 自定义核 0 -1 0 -1 5 -1 0 -1 0 */ Mat kernel = new Mat(3, 3, CvType.CV_16SC1); kernel.put(0, 0, 0, -1, 0, -1, 5, -1, 0, -1, 0); // 对图像和自定义核做卷积 Imgproc.filter2D(src, src, src.depth(), kernel); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } 调用 // 图片处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); // 锐化图片 mBlurUtil.filter2D(mSelectImage);
纯粹阅读,请移步OpenCV中值模糊方法 效果图 源码 KqwOpenCVBlurDemo 步骤 将获取到的Bitmap图片转成Mat对象 // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); 调用OpenCV的中值模糊方法 // 中值模糊方法 Imgproc.medianBlur(src, src, 33); 将处理完的Mat数据转成Bitmap对象 // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); 封装 这里我用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 package kong.qingwei.kqwopencvblurdemo; import android.graphics.Bitmap; import org.opencv.android.Utils; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; import rx.schedulers.Schedulers; /** * Created by kqw on 2016/8/17. * 图片虚化的工具类 */ public class BlurUtil { private Subscriber<Bitmap> mSubscriber; public BlurUtil(Subscriber<Bitmap> subscriber) { mSubscriber = subscriber; } /** * 中值模糊方法 * * @param bitmap 要处理的图片 */ public void medianBlur(Bitmap bitmap) { // RxJava处理图片虚化 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 中值模糊方法 Imgproc.medianBlur(src, src, 33); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } } 工具类的初始化 图片处理在子线程处理,处理完通过回调返回。 // 图片模糊处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); 图片模糊处理 // 中值模糊算法处理图片 mBlurUtil.medianBlur(mSelectImage);
纯粹阅读,请移步OpenCV高斯模糊方法 效果图 源码 KqwOpenCVBlurDemo 步骤 将获取到的Bitmap图片转成Mat对象 // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); 调用OpenCV的高斯模糊方法 // 高斯模糊方法 Imgproc.GaussianBlur(src, src, new Size(91, 91), 0); 将处理完的Mat数据转成Bitmap对象 // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); 封装 这里我用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 package kong.qingwei.kqwopencvblurdemo; import android.graphics.Bitmap; import org.opencv.android.Utils; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; import rx.schedulers.Schedulers; /** * Created by kqw on 2016/8/17. * 图片虚化的工具类 */ public class BlurUtil { private Subscriber<Bitmap> mSubscriber; public BlurUtil(Subscriber<Bitmap> subscriber) { mSubscriber = subscriber; } /** * 高斯模糊方法 * * @param bitmap 要处理的图片 */ public void gaussianBlur(Bitmap bitmap) { // RxJava处理图片虚化 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 高斯模糊方法 Imgproc.GaussianBlur(src, src, new Size(91, 91), 0); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } } 工具类的初始化 图片处理在子线程处理,处理完通过回调返回。 // 图片模糊处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); 图片模糊处理 // 高斯模糊算法处理图片 mBlurUtil.gaussianBlur(mSelectImage);
存粹越多,请访问OpenCV均值模糊方法 效果图 源码 KqwOpenCVBlurDemo 步骤 将获取到的Bitmap图片转成Mat对象 // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); 调用OpenCV的均值模糊方法 // 均值模糊方法 Imgproc.blur(src, src, new Size(100, 100)); 将处理完的Mat数据转成Bitmap对象 // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); 封装 这里我用到了RxJava。主要是因为图片处理是耗时操作,会阻塞线程,为了防止界面卡顿,这里使用RxJava进行了线程切换。 package kong.qingwei.kqwopencvblurdemo; import android.graphics.Bitmap; import org.opencv.android.Utils; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; import rx.schedulers.Schedulers; /** * Created by kqw on 2016/8/17. * 图片虚化的工具类 */ public class BlurUtil { private Subscriber<Bitmap> mSubscriber; public BlurUtil(Subscriber<Bitmap> subscriber) { mSubscriber = subscriber; } public void blur(Bitmap bitmap) { // RxJava处理图片虚化 if (null != mSubscriber) Observable .just(bitmap) .map(new Func1<Bitmap, Bitmap>() { @Override public Bitmap call(Bitmap bitmap) { // Bitmap转为Mat Mat src = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4); Utils.bitmapToMat(bitmap, src); // 均值模糊方法 Imgproc.blur(src, src, new Size(100, 100)); // Mat转Bitmap Bitmap processedImage = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888); Utils.matToBitmap(src, processedImage); return processedImage; } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(mSubscriber); } } 工具类的初始化 图片处理在子线程处理,处理完通过回调返回。 // 图片模糊处理的工具类 mBlurUtil = new BlurUtil(new Subscriber<Bitmap>() { @Override public void onCompleted() { // 图片处理完成 dismissProgressDialog(); } @Override public void onError(Throwable e) { // 图片处理异常 dismissProgressDialog(); } @Override public void onNext(Bitmap bitmap) { // 获取到处理后的图片 mIvImageProcessed.setImageBitmap(bitmap); } }); 图片模糊处理 // 均值模糊算法模糊图片 mBlurUtil.blur(mSelectImage);
灵云语音唤醒 严格来讲,灵云没有语音唤醒功能,但是通过命令词的方式可以实现 实现方式 用命令词的方式实现唤醒,用唤醒词做命令词,做好标识,然后循环开启命令词识别,当识别到唤醒的命令词的时候,视为被唤醒了。 我个人认为这种方式实现唤醒意义不大,所以没有做demo,原因如下: 录音都有前置端点时间1和后置端点时间2,这样一来,即使我们准确的说出唤醒词,也要在说完唤醒词以后,保证在后端点时间内不再有声音录入,才能提高被唤醒的几率。 唤醒不是即时的,即使我们准确的说出唤醒词,并保持不再有声音录入,也要至少等待后端点时间过去,录音结束以后,才会识别(被唤醒)。 这种方式的唤醒其实就是在进行语音识别,如果一直开启唤醒,消耗性能,耗电。 在前置端点时间内没有有效的声音录入,表示此次录音无效,结束录音. ↩ 录音结束以后,在后置端点时间内没有再录入有效声音,表示录音完成,结束录音。 ↩
源码 GitHub 在线语法识别 SDK下载 灵云SDK下载 SDK集成 下载SDK以后,将jar和so导入工程 权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> 封装 灵云配置类 package kong.qingwei.kqwhcidemo; /** * Created by kqw on 2016/8/12. * 灵云配置信息 */ public final class ConfigUtil { /** * 灵云APP_KEY */ public static final String APP_KEY = "3d5d5466"; /** * 开发者密钥 */ public static final String DEVELOPER_KEY = "eca643ff7b3c758745d7cf516e808d34"; /** * 灵云云服务的接口地址 */ public static final String CLOUD_URL = "test.api.hcicloud.com:8888"; /** * 需要运行的灵云能力 */ // 离线语音合成 public static final String CAP_KEY_TTS_LOCAL = "tts.local.synth"; // 云端语音合成 public static final String CAP_KEY_TTS_CLOUD = "tts.cloud.wangjing"; // 云端语义识别 public static final String CAP_KEY_NUL_CLOUD = "nlu.cloud"; // 云端自由说 public static final String CAP_KEY_ASR_CLOUD_FREETALK = "asr.cloud.freetalk"; // 离线自由说 public static final String CAP_KEY_ASR_LOCAL_FREETALK = "asr.local.freetalk"; // 云端语音识别+语义 public static final String CAP_KEY_ASR_CLOUD_DIALOG = "asr.cloud.dialog"; // 离线命令词 public static final String CAP_KEY_ASR_LOCAL_GRAMMAR = "asr.local.grammar.v4"; // 在线命令词 public static final String CAP_KEY_ASR_CLOUD_GRAMMAR = "asr.cloud.grammar"; } 初始化灵云语音能力的工具类 package kong.qingwei.kqwhcidemo; import android.app.Activity; import android.os.Environment; import android.util.Log; import android.widget.Toast; import com.sinovoice.hcicloudsdk.api.HciCloudSys; import com.sinovoice.hcicloudsdk.common.AuthExpireTime; import com.sinovoice.hcicloudsdk.common.HciErrorCode; import com.sinovoice.hcicloudsdk.common.InitParam; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * Created by kqw on 2016/8/12. * 初始化灵云语音 */ public class HciUtil { private static final String TAG = "HciUtil"; private Activity mActivity; private final String mConfigStr; public HciUtil(Activity activity) { mActivity = activity; // 加载信息,返回InitParam, 获得配置参数的字符串 InitParam initParam = getInitParam(); mConfigStr = initParam.getStringConfig(); } public boolean initHci() { // 初始化 int errCode = HciCloudSys.hciInit(mConfigStr, mActivity); if (errCode != HciErrorCode.HCI_ERR_NONE && errCode != HciErrorCode.HCI_ERR_SYS_ALREADY_INIT) { Toast.makeText(mActivity, "hciInit error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); return false; } // 获取授权/更新授权文件 : errCode = checkAuthAndUpdateAuth(); if (errCode != HciErrorCode.HCI_ERR_NONE) { // 由于系统已经初始化成功,在结束前需要调用方法hciRelease()进行系统的反初始化 Toast.makeText(mActivity, "CheckAuthAndUpdateAuth error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); HciCloudSys.hciRelease(); return false; } return true; } /** * 释放 */ public void hciRelease(){ HciCloudSys.hciRelease(); } /** * 加载初始化信息 * * @return 系统初始化参数 */ private InitParam getInitParam() { String authDirPath = mActivity.getFilesDir().getAbsolutePath(); // 前置条件:无 InitParam initparam = new InitParam(); // 授权文件所在路径,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTH_PATH, authDirPath); // 是否自动访问云授权,详见 获取授权/更新授权文件处注释 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTO_CLOUD_AUTH, "no"); // 灵云云服务的接口地址,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_CLOUD_URL, ConfigUtil.CLOUD_URL); // 开发者Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_DEVELOPER_KEY, ConfigUtil.DEVELOPER_KEY); // 应用Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_APP_KEY, ConfigUtil.APP_KEY); // 配置日志参数 String sdcardState = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(sdcardState)) { String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String packageName = mActivity.getPackageName(); String logPath = sdPath + File.separator + "sinovoice" + File.separator + packageName + File.separator + "log" + File.separator; // 日志文件地址 File fileDir = new File(logPath); if (!fileDir.exists()) { fileDir.mkdirs(); } // 日志的路径,可选,如果不传或者为空则不生成日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_PATH, logPath); // 日志数目,默认保留多少个日志文件,超过则覆盖最旧的日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_COUNT, "5"); // 日志大小,默认一个日志文件写多大,单位为K initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_SIZE, "1024"); // 日志等级,0=无,1=错误,2=警告,3=信息,4=细节,5=调试,SDK将输出小于等于logLevel的日志信息 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_LEVEL, "5"); } return initparam; } /** * 获取授权 * * @return 授权结果 */ private int checkAuthAndUpdateAuth() { // 获取系统授权到期时间 int initResult; AuthExpireTime objExpireTime = new AuthExpireTime(); initResult = HciCloudSys.hciGetAuthExpireTime(objExpireTime); if (initResult == HciErrorCode.HCI_ERR_NONE) { // 显示授权日期,如用户不需要关注该值,此处代码可忽略 Date date = new Date(objExpireTime.getExpireTime() * 1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); Log.i(TAG, "expire time: " + sdf.format(date)); if (objExpireTime.getExpireTime() * 1000 > System.currentTimeMillis()) { // 已经成功获取了授权,并且距离授权到期有充足的时间(>7天) Log.i(TAG, "checkAuth success"); return initResult; } } // 获取过期时间失败或者已经过期 initResult = HciCloudSys.hciCheckAuth(); if (initResult == HciErrorCode.HCI_ERR_NONE) { Log.i(TAG, "checkAuth success"); return initResult; } else { Log.e(TAG, "checkAuth failed: " + initResult); return initResult; } } } 语法识别的类 和之前的语音识别一样,只不过配置了语法 在原来的基础上添加了initGrammar方法 package kong.qingwei.kqwhcidemo; import android.app.Activity; import android.util.Log; import com.sinovoice.hcicloudsdk.android.asr.recorder.ASRRecorder; import com.sinovoice.hcicloudsdk.common.asr.AsrConfig; import com.sinovoice.hcicloudsdk.common.asr.AsrGrammarId; import com.sinovoice.hcicloudsdk.common.asr.AsrInitParam; import com.sinovoice.hcicloudsdk.common.asr.AsrRecogResult; import com.sinovoice.hcicloudsdk.recorder.ASRCommonRecorder; import com.sinovoice.hcicloudsdk.recorder.ASRRecorderListener; import com.sinovoice.hcicloudsdk.recorder.RecorderEvent; import java.io.IOException; import java.io.InputStream; /** * Created by kqw on 2016/8/15. * 语音识别类 */ public class AsrUtil { private static final String TAG = "AsrUtil"; private Activity mActivity; private ASRRecorder mAsrRecorder; private AsrConfig asrConfig; private OnAsrRecogListener mOnAsrRecogListener; private String mGrammar = null; private String mCapKey = ConfigUtil.CAP_KEY_ASR_CLOUD_GRAMMAR; public AsrUtil(Activity activity) { mActivity = activity; initAsr(); initGrammar(mCapKey); } /** * 初始化语音识别 */ private void initAsr() { Log.i(TAG, "initAsr: "); // 初始化录音机 mAsrRecorder = new ASRRecorder(); // 配置初始化参数 AsrInitParam asrInitParam = new AsrInitParam(); String dataPath = mActivity.getFilesDir().getPath().replace("files", "lib"); asrInitParam.addParam(AsrInitParam.PARAM_KEY_INIT_CAP_KEYS, mCapKey); asrInitParam.addParam(AsrInitParam.PARAM_KEY_DATA_PATH, dataPath); asrInitParam.addParam(AsrInitParam.PARAM_KEY_FILE_FLAG, AsrInitParam.VALUE_OF_PARAM_FILE_FLAG_ANDROID_SO); Log.v(TAG, "init parameters:" + asrInitParam.getStringConfig()); // 设置初始化参数 mAsrRecorder.init(asrInitParam.getStringConfig(), new ASRResultProcess()); // 配置识别参数 asrConfig = new AsrConfig(); // PARAM_KEY_CAP_KEY 设置使用的能力 asrConfig.addParam(AsrConfig.SessionConfig.PARAM_KEY_CAP_KEY, mCapKey); // PARAM_KEY_AUDIO_FORMAT 音频格式根据不同的能力使用不用的音频格式 asrConfig.addParam(AsrConfig.AudioConfig.PARAM_KEY_AUDIO_FORMAT, AsrConfig.AudioConfig.VALUE_OF_PARAM_AUDIO_FORMAT_PCM_16K16BIT); // PARAM_KEY_ENCODE 音频编码压缩格式,使用OPUS可以有效减小数据流量 asrConfig.addParam(AsrConfig.AudioConfig.PARAM_KEY_ENCODE, AsrConfig.AudioConfig.VALUE_OF_PARAM_ENCODE_SPEEX); // 其他配置,此处可以全部选取缺省值 asrConfig.addParam("intention", "weather"); } /** * 初始化语法 * * @param capKey CapKey */ public void initGrammar(String capKey) { // 语法相关的配置,若使用自由说能力可以不必配置该项 if (capKey.contains("local.grammar")) { mGrammar = loadGrammar("stock_10001.gram"); // 加载本地语法获取语法ID AsrGrammarId id = new AsrGrammarId(); ASRCommonRecorder.loadGrammar("capkey=" + capKey + ",grammarType=jsgf", mGrammar, id); Log.d(TAG, "grammarid=" + id); // PARAM_KEY_GRAMMAR_TYPE 语法类型,使用自由说能力时,忽略以下此参数 asrConfig.addParam(AsrConfig.GrammarConfig.PARAM_KEY_GRAMMAR_TYPE, AsrConfig.GrammarConfig.VALUE_OF_PARAM_GRAMMAR_TYPE_ID); asrConfig.addParam(AsrConfig.GrammarConfig.PARAM_KEY_GRAMMAR_ID, "" + id.getGrammarId()); } else if (capKey.contains("cloud.grammar")) { mGrammar = loadGrammar("stock_10001.gram"); // PARAM_KEY_GRAMMAR_TYPE 语法类型,使用自由说能力时,忽略以下此参数 asrConfig.addParam(AsrConfig.GrammarConfig.PARAM_KEY_GRAMMAR_TYPE, AsrConfig.GrammarConfig.VALUE_OF_PARAM_GRAMMAR_TYPE_JSGF); } } /** * 开始语音识别 */ public void start(OnAsrRecogListener listener) { mOnAsrRecogListener = listener; if (mAsrRecorder.getRecorderState() == ASRRecorder.RECORDER_STATE_IDLE) { asrConfig.addParam(AsrConfig.SessionConfig.PARAM_KEY_REALTIME, "no"); mAsrRecorder.start(asrConfig.getStringConfig(), mGrammar); } else { Log.i(TAG, "start: 录音机未处于空闲状态,请稍等"); } } private class ASRResultProcess implements ASRRecorderListener { @Override public void onRecorderEventError(RecorderEvent event, int errorCode) { Log.i(TAG, "onRecorderEventError: errorCode = " + errorCode); if (null != mOnAsrRecogListener) { mOnAsrRecogListener.onError(errorCode); } } @Override public void onRecorderEventRecogFinsh(RecorderEvent recorderEvent, final AsrRecogResult arg1) { if (recorderEvent == RecorderEvent.RECORDER_EVENT_RECOGNIZE_COMPLETE) { Log.i(TAG, "onRecorderEventRecogFinsh: 识别结束"); } if (null != mOnAsrRecogListener) { mActivity.runOnUiThread(new Runnable() { @Override public void run() { mOnAsrRecogListener.onAsrRecogResult(arg1); } }); } } @Override public void onRecorderEventStateChange(RecorderEvent recorderEvent) { if (recorderEvent == RecorderEvent.RECORDER_EVENT_BEGIN_RECORD) { Log.i(TAG, "onRecorderEventStateChange: 开始录音"); } else if (recorderEvent == RecorderEvent.RECORDER_EVENT_BEGIN_RECOGNIZE) { Log.i(TAG, "onRecorderEventStateChange: 开始识别"); } else if (recorderEvent == RecorderEvent.RECORDER_EVENT_NO_VOICE_INPUT) { Log.i(TAG, "onRecorderEventStateChange: 无音频输入"); } else { Log.i(TAG, "onRecorderEventStateChange: recorderEvent = " + recorderEvent); } } @Override public void onRecorderRecording(byte[] volumedata, int volume) { if (null != mOnAsrRecogListener) { mOnAsrRecogListener.onVolume(volume); } } @Override public void onRecorderEventRecogProcess(RecorderEvent recorderEvent, AsrRecogResult arg1) { if (recorderEvent == RecorderEvent.RECORDER_EVENT_RECOGNIZE_PROCESS) { Log.i(TAG, "onRecorderEventRecogProcess: 识别中间反馈"); } if (arg1 != null) { if (arg1.getRecogItemList().size() > 0) { Log.i(TAG, "onRecorderEventRecogProcess: 识别中间结果结果为:" + arg1.getRecogItemList().get(0).getRecogResult()); } else { Log.i(TAG, "onRecorderEventRecogProcess: 未能正确识别,请重新输入"); } } } } /** * 读取语法 * * @param fileName 文件名 * @return 语法 */ private String loadGrammar(String fileName) { String grammar = ""; InputStream is = null; try { is = mActivity.getAssets().open(fileName); byte[] data = new byte[is.available()]; is.read(data); grammar = new String(data); } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return grammar; } /** * 语音识别的回调接口 */ public interface OnAsrRecogListener { // 识别结果 void onAsrRecogResult(AsrRecogResult asrRecogResult); // 识别错误码 void onError(int errorCode); // 录音音量 void onVolume(int volume); } } 使用 使用和语音识别完全一致 初始化灵云的语音能力和语法识别 // 灵云语音工具类 mInitTts = new HciUtil(this); // 初始化灵云语音 boolean isInitHci = mInitTts.initHci(); if (isInitHci) { // 初始化成功 …… // 语音识别 mAsrUtil = new AsrUtil(this); } 语法识别 /** * 语音识别(语音转文字) * * @param view view */ public void asr(View view) { mAsrUtil.start(new AsrUtil.OnAsrRecogListener() { @Override public void onAsrRecogResult(AsrRecogResult asrRecogResult) { StringBuilder stringBuffer = new StringBuilder(); ArrayList<AsrRecogItem> asrRecogItemArrayList = asrRecogResult.getRecogItemList(); for (AsrRecogItem asrRecogItem : asrRecogItemArrayList) { String result = asrRecogItem.getRecogResult(); Log.i(TAG, "onAsrRecogResult: " + result); stringBuffer.append(result).append("\n"); } showDialog("识别结果", stringBuffer.toString()); } @Override public void onError(int errorCode) { Log.i(TAG, "onError: " + errorCode); } @Override public void onVolume(int volume) { Log.i(TAG, "onVolume: " + volume); } }); } 离线语音识别 在在线语法识别的基础上,离线的语法识别只需要将CapKey替换,并且添加离线资源即可 离线资源的下载 下载完解压离线资源 将里面所有的文件重命名,前面加lib,后面加.so,然后导入工程 修改CapKey为asr.local.grammar.v4 注意,灵云的离线语音功能第一次使用需要联网激活,激活以后才可以使用离线功能。
源码 GitHub 在线语音识别 SDK下载 灵云SDK下载 SDK集成 下载SDK以后,将jar和so导入工程 权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> 封装 灵云配置类 package kong.qingwei.kqwhcidemo; /** * Created by kqw on 2016/8/12. * 灵云配置信息 */ public final class ConfigUtil { /** * 灵云APP_KEY */ public static final String APP_KEY = "3d5d5466"; /** * 开发者密钥 */ public static final String DEVELOPER_KEY = "eca643ff7b3c758745d7cf516e808d34"; /** * 灵云云服务的接口地址 */ public static final String CLOUD_URL = "test.api.hcicloud.com:8888"; /** * 需要运行的灵云能力 */ public static final String CAP_KEY = "tts.local.synth"; // public static final String CAP_KEY = "tts.cloud.wangjing"; public static final String CAP_KEY_NUL = "nlu.cloud"; } 初始化灵云语音能力的工具类 package kong.qingwei.kqwhcidemo; import android.app.Activity; import android.os.Environment; import android.util.Log; import android.widget.Toast; import com.sinovoice.hcicloudsdk.api.HciCloudSys; import com.sinovoice.hcicloudsdk.common.AuthExpireTime; import com.sinovoice.hcicloudsdk.common.HciErrorCode; import com.sinovoice.hcicloudsdk.common.InitParam; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * Created by kqw on 2016/8/12. * 初始化灵云语音 */ public class HciUtil { private static final String TAG = "HciUtil"; private Activity mActivity; private final String mConfigStr; public HciUtil(Activity activity) { mActivity = activity; // 加载信息,返回InitParam, 获得配置参数的字符串 InitParam initParam = getInitParam(); mConfigStr = initParam.getStringConfig(); } public boolean initHci() { // 初始化 int errCode = HciCloudSys.hciInit(mConfigStr, mActivity); if (errCode != HciErrorCode.HCI_ERR_NONE && errCode != HciErrorCode.HCI_ERR_SYS_ALREADY_INIT) { Toast.makeText(mActivity, "hciInit error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); return false; } // 获取授权/更新授权文件 : errCode = checkAuthAndUpdateAuth(); if (errCode != HciErrorCode.HCI_ERR_NONE) { // 由于系统已经初始化成功,在结束前需要调用方法hciRelease()进行系统的反初始化 Toast.makeText(mActivity, "CheckAuthAndUpdateAuth error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); HciCloudSys.hciRelease(); return false; } return true; } /** * 释放 */ public void hciRelease(){ HciCloudSys.hciRelease(); } /** * 加载初始化信息 * * @return 系统初始化参数 */ private InitParam getInitParam() { String authDirPath = mActivity.getFilesDir().getAbsolutePath(); // 前置条件:无 InitParam initparam = new InitParam(); // 授权文件所在路径,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTH_PATH, authDirPath); // 是否自动访问云授权,详见 获取授权/更新授权文件处注释 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTO_CLOUD_AUTH, "no"); // 灵云云服务的接口地址,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_CLOUD_URL, ConfigUtil.CLOUD_URL); // 开发者Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_DEVELOPER_KEY, ConfigUtil.DEVELOPER_KEY); // 应用Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_APP_KEY, ConfigUtil.APP_KEY); // 配置日志参数 String sdcardState = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(sdcardState)) { String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String packageName = mActivity.getPackageName(); String logPath = sdPath + File.separator + "sinovoice" + File.separator + packageName + File.separator + "log" + File.separator; // 日志文件地址 File fileDir = new File(logPath); if (!fileDir.exists()) { fileDir.mkdirs(); } // 日志的路径,可选,如果不传或者为空则不生成日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_PATH, logPath); // 日志数目,默认保留多少个日志文件,超过则覆盖最旧的日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_COUNT, "5"); // 日志大小,默认一个日志文件写多大,单位为K initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_SIZE, "1024"); // 日志等级,0=无,1=错误,2=警告,3=信息,4=细节,5=调试,SDK将输出小于等于logLevel的日志信息 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_LEVEL, "5"); } return initparam; } /** * 获取授权 * * @return 授权结果 */ private int checkAuthAndUpdateAuth() { // 获取系统授权到期时间 int initResult; AuthExpireTime objExpireTime = new AuthExpireTime(); initResult = HciCloudSys.hciGetAuthExpireTime(objExpireTime); if (initResult == HciErrorCode.HCI_ERR_NONE) { // 显示授权日期,如用户不需要关注该值,此处代码可忽略 Date date = new Date(objExpireTime.getExpireTime() * 1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); Log.i(TAG, "expire time: " + sdf.format(date)); if (objExpireTime.getExpireTime() * 1000 > System.currentTimeMillis()) { // 已经成功获取了授权,并且距离授权到期有充足的时间(>7天) Log.i(TAG, "checkAuth success"); return initResult; } } // 获取过期时间失败或者已经过期 initResult = HciCloudSys.hciCheckAuth(); if (initResult == HciErrorCode.HCI_ERR_NONE) { Log.i(TAG, "checkAuth success"); return initResult; } else { Log.e(TAG, "checkAuth failed: " + initResult); return initResult; } } } 语音识别的类 package kong.qingwei.kqwhcidemo; import android.app.Activity; import android.util.Log; import com.sinovoice.hcicloudsdk.android.asr.recorder.ASRRecorder; import com.sinovoice.hcicloudsdk.common.asr.AsrConfig; import com.sinovoice.hcicloudsdk.common.asr.AsrInitParam; import com.sinovoice.hcicloudsdk.common.asr.AsrRecogResult; import com.sinovoice.hcicloudsdk.recorder.ASRRecorderListener; import com.sinovoice.hcicloudsdk.recorder.RecorderEvent; /** * Created by kqw on 2016/8/15. * 语音识别类 */ public class AsrUtil { private static final String TAG = "AsrUtil"; private Activity mActivity; private ASRRecorder mAsrRecorder; private AsrConfig asrConfig; private OnAsrRecogListener mOnAsrRecogListener; public AsrUtil(Activity activity) { mActivity = activity; initAsr(); } private void initAsr() { Log.i(TAG, "initAsr: "); // 初始化录音机 mAsrRecorder = new ASRRecorder(); // 配置初始化参数 AsrInitParam asrInitParam = new AsrInitParam(); String dataPath = mActivity.getFilesDir().getPath().replace("files", "lib"); asrInitParam.addParam(AsrInitParam.PARAM_KEY_INIT_CAP_KEYS, ConfigUtil.CAP_KEY_ASR_CLOUD_FREETALK); asrInitParam.addParam(AsrInitParam.PARAM_KEY_DATA_PATH, dataPath); asrInitParam.addParam(AsrInitParam.PARAM_KEY_FILE_FLAG, AsrInitParam.VALUE_OF_PARAM_FILE_FLAG_ANDROID_SO); Log.v(TAG, "init parameters:" + asrInitParam.getStringConfig()); // 设置初始化参数 mAsrRecorder.init(asrInitParam.getStringConfig(), new ASRResultProcess()); // 配置识别参数 asrConfig = new AsrConfig(); // PARAM_KEY_CAP_KEY 设置使用的能力 asrConfig.addParam(AsrConfig.SessionConfig.PARAM_KEY_CAP_KEY, ConfigUtil.CAP_KEY_ASR_CLOUD_FREETALK); // PARAM_KEY_AUDIO_FORMAT 音频格式根据不同的能力使用不用的音频格式 asrConfig.addParam(AsrConfig.AudioConfig.PARAM_KEY_AUDIO_FORMAT, AsrConfig.AudioConfig.VALUE_OF_PARAM_AUDIO_FORMAT_PCM_16K16BIT); // PARAM_KEY_ENCODE 音频编码压缩格式,使用OPUS可以有效减小数据流量 asrConfig.addParam(AsrConfig.AudioConfig.PARAM_KEY_ENCODE, AsrConfig.AudioConfig.VALUE_OF_PARAM_ENCODE_SPEEX); // 其他配置,此处可以全部选取缺省值 asrConfig.addParam("intention", "weather"); } /** * 开始语音识别 */ public void start(OnAsrRecogListener listener) { mOnAsrRecogListener = listener; if (mAsrRecorder.getRecorderState() == ASRRecorder.RECORDER_STATE_IDLE) { asrConfig.addParam(AsrConfig.SessionConfig.PARAM_KEY_REALTIME, "no"); mAsrRecorder.start(asrConfig.getStringConfig(), null); } else { Log.i(TAG, "start: 录音机未处于空闲状态,请稍等"); } } private class ASRResultProcess implements ASRRecorderListener { @Override public void onRecorderEventError(RecorderEvent event, int errorCode) { Log.i(TAG, "onRecorderEventError: errorCode = " + errorCode); if (null != mOnAsrRecogListener) { mOnAsrRecogListener.onError(errorCode); } } @Override public void onRecorderEventRecogFinsh(RecorderEvent recorderEvent, final AsrRecogResult arg1) { if (recorderEvent == RecorderEvent.RECORDER_EVENT_RECOGNIZE_COMPLETE) { Log.i(TAG, "onRecorderEventRecogFinsh: 识别结束"); } if (null != mOnAsrRecogListener) { mActivity.runOnUiThread(new Runnable() { @Override public void run() { mOnAsrRecogListener.onAsrRecogResult(arg1); } }); } } @Override public void onRecorderEventStateChange(RecorderEvent recorderEvent) { if (recorderEvent == RecorderEvent.RECORDER_EVENT_BEGIN_RECORD) { Log.i(TAG, "onRecorderEventStateChange: 开始录音"); } else if (recorderEvent == RecorderEvent.RECORDER_EVENT_BEGIN_RECOGNIZE) { Log.i(TAG, "onRecorderEventStateChange: 开始识别"); } else if (recorderEvent == RecorderEvent.RECORDER_EVENT_NO_VOICE_INPUT) { Log.i(TAG, "onRecorderEventStateChange: 无音频输入"); } else { Log.i(TAG, "onRecorderEventStateChange: recorderEvent = " + recorderEvent); } } @Override public void onRecorderRecording(byte[] volumedata, int volume) { if (null != mOnAsrRecogListener) { mOnAsrRecogListener.onVolume(volume); } } @Override public void onRecorderEventRecogProcess(RecorderEvent recorderEvent, AsrRecogResult arg1) { if (recorderEvent == RecorderEvent.RECORDER_EVENT_RECOGNIZE_PROCESS) { Log.i(TAG, "onRecorderEventRecogProcess: 识别中间反馈"); } if (arg1 != null) { if (arg1.getRecogItemList().size() > 0) { Log.i(TAG, "onRecorderEventRecogProcess: 识别中间结果结果为:" + arg1.getRecogItemList().get(0).getRecogResult()); } else { Log.i(TAG, "onRecorderEventRecogProcess: 未能正确识别,请重新输入"); } } } } /** * 语音识别的回调接口 */ public interface OnAsrRecogListener { // 识别结果 void onAsrRecogResult(AsrRecogResult asrRecogResult); // 识别错误码 void onError(int errorCode); // 录音音量 void onVolume(int volume); } } 使用 初始化灵云的语音能力和语音识别 // 灵云语音工具类 mInitTts = new HciUtil(this); // 初始化灵云语音 boolean isInitHci = mInitTts.initHci(); if (isInitHci) { // 初始化成功 …… // 语音识别 mAsrUtil = new AsrUtil(this); } 语音识别 /** * 语音识别(语音转文字) * * @param view view */ public void asr(View view) { mAsrUtil.start(new AsrUtil.OnAsrRecogListener() { @Override public void onAsrRecogResult(AsrRecogResult asrRecogResult) { StringBuilder stringBuffer = new StringBuilder(); ArrayList<AsrRecogItem> asrRecogItemArrayList = asrRecogResult.getRecogItemList(); for (AsrRecogItem asrRecogItem : asrRecogItemArrayList) { String result = asrRecogItem.getRecogResult(); Log.i(TAG, "onAsrRecogResult: " + result); stringBuffer.append(result).append("\n"); } showDialog("识别结果", stringBuffer.toString()); } @Override public void onError(int errorCode) { Log.i(TAG, "onError: " + errorCode); } @Override public void onVolume(int volume) { Log.i(TAG, "onVolume: " + volume); } }); } 离线语音识别 离线命令词和在线很类似,只需要更改CapKey,导入离线资源包即可,我们先下载离线资源 下载完解压 将里面所有的文件重命名,前面加lib,后面加.so,然后导入工程 修改CapKey为asr.local.freetalk 注意,灵云的离线语音功能第一次使用需要联网激活,激活以后才可以使用离线功能。 在线语音识别 + 语义理解 直接在在线语音识别的基础上实现语义理解更加简单,只要将CapKey换成asr.cloud.dialog即可 但是需要注意一点的是,想要使用哪个场景,必须开通以后,在参数里加上对应的场景,才是识别出来。 类似这样 asrConfig.addParam("intention", "weather");
效果图 源码 GitHub SDK下载 灵云SDK下载 SDK集成 下载SDK以后,将jar和so导入工程 权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 封装 灵云配置类 package kong.qingwei.kqwhcidemo; /** * Created by kqw on 2016/8/12. * 灵云配置信息 */ public final class ConfigUtil { /** * 灵云APP_KEY */ public static final String APP_KEY = "3d5d5466"; /** * 开发者密钥 */ public static final String DEVELOPER_KEY = "eca643ff7b3c758745d7cf516e808d34"; /** * 灵云云服务的接口地址 */ public static final String CLOUD_URL = "test.api.hcicloud.com:8888"; /** * 需要运行的灵云能力 */ public static final String CAP_KEY = "tts.local.synth"; // public static final String CAP_KEY = "tts.cloud.wangjing"; public static final String CAP_KEY_NUL = "nlu.cloud"; } 初始化灵云语音能力的工具类 package kong.qingwei.kqwhcidemo; import android.app.Activity; import android.os.Environment; import android.util.Log; import android.widget.Toast; import com.sinovoice.hcicloudsdk.api.HciCloudSys; import com.sinovoice.hcicloudsdk.common.AuthExpireTime; import com.sinovoice.hcicloudsdk.common.HciErrorCode; import com.sinovoice.hcicloudsdk.common.InitParam; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * Created by kqw on 2016/8/12. * 初始化灵云语音 */ public class HciUtil { private static final String TAG = "HciUtil"; private Activity mActivity; private final String mConfigStr; public HciUtil(Activity activity) { mActivity = activity; // 加载信息,返回InitParam, 获得配置参数的字符串 InitParam initParam = getInitParam(); mConfigStr = initParam.getStringConfig(); } public boolean initHci() { // 初始化 int errCode = HciCloudSys.hciInit(mConfigStr, mActivity); if (errCode != HciErrorCode.HCI_ERR_NONE && errCode != HciErrorCode.HCI_ERR_SYS_ALREADY_INIT) { Toast.makeText(mActivity, "hciInit error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); return false; } // 获取授权/更新授权文件 : errCode = checkAuthAndUpdateAuth(); if (errCode != HciErrorCode.HCI_ERR_NONE) { // 由于系统已经初始化成功,在结束前需要调用方法hciRelease()进行系统的反初始化 Toast.makeText(mActivity, "CheckAuthAndUpdateAuth error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); HciCloudSys.hciRelease(); return false; } return true; } /** * 释放 */ public void hciRelease(){ HciCloudSys.hciRelease(); } /** * 加载初始化信息 * * @return 系统初始化参数 */ private InitParam getInitParam() { String authDirPath = mActivity.getFilesDir().getAbsolutePath(); // 前置条件:无 InitParam initparam = new InitParam(); // 授权文件所在路径,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTH_PATH, authDirPath); // 是否自动访问云授权,详见 获取授权/更新授权文件处注释 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTO_CLOUD_AUTH, "no"); // 灵云云服务的接口地址,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_CLOUD_URL, ConfigUtil.CLOUD_URL); // 开发者Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_DEVELOPER_KEY, ConfigUtil.DEVELOPER_KEY); // 应用Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_APP_KEY, ConfigUtil.APP_KEY); // 配置日志参数 String sdcardState = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(sdcardState)) { String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String packageName = mActivity.getPackageName(); String logPath = sdPath + File.separator + "sinovoice" + File.separator + packageName + File.separator + "log" + File.separator; // 日志文件地址 File fileDir = new File(logPath); if (!fileDir.exists()) { fileDir.mkdirs(); } // 日志的路径,可选,如果不传或者为空则不生成日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_PATH, logPath); // 日志数目,默认保留多少个日志文件,超过则覆盖最旧的日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_COUNT, "5"); // 日志大小,默认一个日志文件写多大,单位为K initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_SIZE, "1024"); // 日志等级,0=无,1=错误,2=警告,3=信息,4=细节,5=调试,SDK将输出小于等于logLevel的日志信息 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_LEVEL, "5"); } return initparam; } /** * 获取授权 * * @return 授权结果 */ private int checkAuthAndUpdateAuth() { // 获取系统授权到期时间 int initResult; AuthExpireTime objExpireTime = new AuthExpireTime(); initResult = HciCloudSys.hciGetAuthExpireTime(objExpireTime); if (initResult == HciErrorCode.HCI_ERR_NONE) { // 显示授权日期,如用户不需要关注该值,此处代码可忽略 Date date = new Date(objExpireTime.getExpireTime() * 1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); Log.i(TAG, "expire time: " + sdf.format(date)); if (objExpireTime.getExpireTime() * 1000 > System.currentTimeMillis()) { // 已经成功获取了授权,并且距离授权到期有充足的时间(>7天) Log.i(TAG, "checkAuth success"); return initResult; } } // 获取过期时间失败或者已经过期 initResult = HciCloudSys.hciCheckAuth(); if (initResult == HciErrorCode.HCI_ERR_NONE) { Log.i(TAG, "checkAuth success"); return initResult; } else { Log.e(TAG, "checkAuth failed: " + initResult); return initResult; } } } 语义理解的类 package kong.qingwei.kqwhcidemo; import android.app.Activity; import android.util.Log; import com.sinovoice.hcicloudsdk.api.nlu.HciCloudNlu; import com.sinovoice.hcicloudsdk.common.HciErrorCode; import com.sinovoice.hcicloudsdk.common.Session; import com.sinovoice.hcicloudsdk.common.nlu.NluConfig; import com.sinovoice.hcicloudsdk.common.nlu.NluInitParam; import com.sinovoice.hcicloudsdk.common.nlu.NluRecogResult; /** * Created by kqw on 2016/8/12. * 语义理解类 */ public class NluUtil { private static final String TAG = "NluUtil"; private Activity mActivity; public NluUtil(Activity activity) { mActivity = activity; } public boolean initNul() { //构造Asr初始化的帮助类的实例 NluInitParam initParam = new NluInitParam(); // 获取App应用中的lib的路径,放置能力所需资源文件。如果使用/data/data/packagename/lib目录,需要添加android_so的标记 String dataPath = mActivity.getFilesDir().getAbsolutePath().replace("files", "lib"); initParam.addParam(NluInitParam.PARAM_KEY_DATA_PATH, dataPath); initParam.addParam(NluInitParam.PARAM_KEY_FILE_FLAG, NluInitParam.VALUE_OF_PARAM_FILE_FLAG_ANDROID_SO); initParam.addParam(NluInitParam.PARAM_KEY_INIT_CAP_KEYS, ConfigUtil.CAP_KEY_NUL); int errCode = HciCloudNlu.hciNluInit(initParam.getStringConfig()); Log.i(TAG, "initNul: errCode = " + errCode); return errCode == HciErrorCode.HCI_ERR_NONE || errCode == HciErrorCode.HCI_ERR_NLU_ALREADY_INIT; } public void recog(String text, OnNluRecogListener onNluRecogListener) { // 初始化配置参数 NluConfig nluConfig = initConfig(); // 创建会话 Session session = new Session(); // 开始会话 int errCode = HciCloudNlu.hciNluSessionStart(nluConfig.getStringConfig(), session); if (errCode == HciErrorCode.HCI_ERR_NONE) { // 开始翻译 // 调用HciCloudMt.hciMtTrans() 方法进行合成 NluRecogResult nluResult = new NluRecogResult(); errCode = HciCloudNlu.hciNluRecog(session, text, nluConfig.getStringConfig(), nluResult); if (errCode == HciErrorCode.HCI_ERR_NONE) { onNluRecogListener.onNluResult(nluResult); } else { onNluRecogListener.onError(errCode); } // 结束会话 errCode = HciCloudNlu.hciNluSessionStop(session); if (errCode != HciErrorCode.HCI_ERR_NONE) { onNluRecogListener.onError(errCode); } } else { onNluRecogListener.onError(errCode); } } private NluConfig initConfig() { NluConfig nluConfig = new NluConfig(); nluConfig.addParam(NluConfig.SessionConfig.PARAM_KEY_CAP_KEY, ConfigUtil.CAP_KEY_NUL); nluConfig.addParam("intention", "weather"); return nluConfig; } public interface OnNluRecogListener { void onNluResult(NluRecogResult nluRecogResult); void onError(int errorCode); } } 使用 初始化灵云的语音能力和语义理解 // 灵云语音工具类 mInitTts = new HciUtil(this); // 初始化灵云语音 boolean isInitHci = mInitTts.initHci(); if (isInitHci) { // 初始化成功 …… // 语义理解 mNluUtil = new NluUtil(this); boolean isInitNul = mNluUtil.initNul(); if (isInitNul) { Toast.makeText(this, "语义理解 初始化成功", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "语义理解 初始化失败", Toast.LENGTH_SHORT).show(); } } 语义理解 /** * 语义理解 * * @param view */ public void recog(View view) { final String text = mEditText2.getText().toString(); if (TextUtils.isEmpty(text)) { Toast.makeText(this, "理解句子内容为空", Toast.LENGTH_SHORT).show(); return; } mNluUtil.recog(text, new NluUtil.OnNluRecogListener() { @Override public void onNluResult(NluRecogResult nluRecogResult) { StringBuilder stringBuffer = new StringBuilder(); ArrayList<NluRecogResultItem> nluRecogResultItems = nluRecogResult.getRecogResultItemList(); for (NluRecogResultItem nluRecogResultItem : nluRecogResultItems) { String result = nluRecogResultItem.getResult(); stringBuffer.append(result).append("\n"); Log.i(TAG, "onNluResult: " + result); } showDialog(text, stringBuffer.toString()); } @Override public void onError(int errorCode) { Log.i(TAG, "onError: errorCode = " + errorCode); } }); } Dialog /** * 显示Dialog * * @param title title * @param message message */ private void showDialog(String title, String message) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(title); builder.setMessage(message); builder.setPositiveButton("确认", null); builder.create().show(); } 注意 灵云的语义,想要识别哪个场景,必须要先开通对应场景,然后做配置以后才可以使用,类似这样 nluConfig.addParam("intention", "weather"); 这里的天气,对应的参数是weather,了解更多场景和对应场景参数可以查看捷通华声灵云公有云能力平台NLU结果开发手册.pdf
注册 官网 注册比较简单,就不做过多介绍了,注册万应用以后,在后台创建自己的应用,创建完应用以后需要给应用开通对应的语音能力。 集成 下载灵云SDK 如果使用在线功能,下载对应的SDK,里面有jar包和so,就可以满足需求了。如果要使用离线的语音功能,还需要下载灵云资源文件 源码 GitHub 灵云在线语音合成 权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 配置类 package kong.qingwei.kqwhcittsdemo; /** * Created by kqw on 2016/8/12. * 灵云配置信息 */ public final class ConfigUtil { /** * 灵云APP_KEY */ public static final String APP_KEY = "填入自己的APP KEY"; /** * 开发者密钥 */ public static final String DEVELOPER_KEY = "填入自己的DEVELOPER KEY"; /** * 灵云云服务的接口地址 */ public static final String CLOUD_URL = "test.api.hcicloud.com:8888"; /** * 需要运行的灵云能力 */ // public static final String CAP_KEY = "tts.local.synth"; public static final String CAP_KEY = "tts.cloud.wangjing"; } 封装灵云语音的初始化 package kong.qingwei.kqwhcittsdemo; import android.app.Activity; import android.os.Environment; import android.util.Log; import android.widget.Toast; import com.sinovoice.hcicloudsdk.api.HciCloudSys; import com.sinovoice.hcicloudsdk.common.AuthExpireTime; import com.sinovoice.hcicloudsdk.common.HciErrorCode; import com.sinovoice.hcicloudsdk.common.InitParam; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * Created by kqw on 2016/8/12. * 初始化灵云语音 */ public class HciUtil { private static final String TAG = "HciUtil"; private Activity mActivity; private final String mConfigStr; public HciUtil(Activity activity) { mActivity = activity; // 加载信息,返回InitParam, 获得配置参数的字符串 InitParam initParam = getInitParam(); mConfigStr = initParam.getStringConfig(); } public boolean initHci() { // 初始化 int errCode = HciCloudSys.hciInit(mConfigStr, mActivity); if (errCode != HciErrorCode.HCI_ERR_NONE && errCode != HciErrorCode.HCI_ERR_SYS_ALREADY_INIT) { Toast.makeText(mActivity, "hciInit error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); return false; } // 获取授权/更新授权文件 : errCode = checkAuthAndUpdateAuth(); if (errCode != HciErrorCode.HCI_ERR_NONE) { // 由于系统已经初始化成功,在结束前需要调用方法hciRelease()进行系统的反初始化 Toast.makeText(mActivity, "CheckAuthAndUpdateAuth error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); HciCloudSys.hciRelease(); return false; } return true; } /** * 加载初始化信息 * * @return 系统初始化参数 */ private InitParam getInitParam() { String authDirPath = mActivity.getFilesDir().getAbsolutePath(); // 前置条件:无 InitParam initparam = new InitParam(); // 授权文件所在路径,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTH_PATH, authDirPath); // 是否自动访问云授权,详见 获取授权/更新授权文件处注释 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTO_CLOUD_AUTH, "no"); // 灵云云服务的接口地址,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_CLOUD_URL, ConfigUtil.CLOUD_URL); // 开发者Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_DEVELOPER_KEY, ConfigUtil.DEVELOPER_KEY); // 应用Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_APP_KEY, ConfigUtil.APP_KEY); // 配置日志参数 String sdcardState = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(sdcardState)) { String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String packageName = mActivity.getPackageName(); String logPath = sdPath + File.separator + "sinovoice" + File.separator + packageName + File.separator + "log" + File.separator; // 日志文件地址 File fileDir = new File(logPath); if (!fileDir.exists()) { fileDir.mkdirs(); } // 日志的路径,可选,如果不传或者为空则不生成日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_PATH, logPath); // 日志数目,默认保留多少个日志文件,超过则覆盖最旧的日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_COUNT, "5"); // 日志大小,默认一个日志文件写多大,单位为K initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_SIZE, "1024"); // 日志等级,0=无,1=错误,2=警告,3=信息,4=细节,5=调试,SDK将输出小于等于logLevel的日志信息 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_LEVEL, "5"); } return initparam; } /** * 获取授权 * * @return 授权结果 */ private int checkAuthAndUpdateAuth() { // 获取系统授权到期时间 int initResult; AuthExpireTime objExpireTime = new AuthExpireTime(); initResult = HciCloudSys.hciGetAuthExpireTime(objExpireTime); if (initResult == HciErrorCode.HCI_ERR_NONE) { // 显示授权日期,如用户不需要关注该值,此处代码可忽略 Date date = new Date(objExpireTime.getExpireTime() * 1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); Log.i(TAG, "expire time: " + sdf.format(date)); if (objExpireTime.getExpireTime() * 1000 > System.currentTimeMillis()) { // 已经成功获取了授权,并且距离授权到期有充足的时间(>7天) Log.i(TAG, "checkAuth success"); return initResult; } } // 获取过期时间失败或者已经过期 initResult = HciCloudSys.hciCheckAuth(); if (initResult == HciErrorCode.HCI_ERR_NONE) { Log.i(TAG, "checkAuth success"); return initResult; } else { Log.e(TAG, "checkAuth failed: " + initResult); return initResult; } } } 封装灵云语音合成能 package kong.qingwei.kqwhcittsdemo; import android.app.Activity; import android.os.Environment; import android.util.Log; import android.widget.Toast; import com.sinovoice.hcicloudsdk.api.HciCloudSys; import com.sinovoice.hcicloudsdk.common.AuthExpireTime; import com.sinovoice.hcicloudsdk.common.HciErrorCode; import com.sinovoice.hcicloudsdk.common.InitParam; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * Created by kqw on 2016/8/12. * 初始化灵云语音 */ public class HciUtil { private static final String TAG = "HciUtil"; private Activity mActivity; private final String mConfigStr; public HciUtil(Activity activity) { mActivity = activity; // 加载信息,返回InitParam, 获得配置参数的字符串 InitParam initParam = getInitParam(); mConfigStr = initParam.getStringConfig(); } public boolean initHci() { // 初始化 int errCode = HciCloudSys.hciInit(mConfigStr, mActivity); if (errCode != HciErrorCode.HCI_ERR_NONE && errCode != HciErrorCode.HCI_ERR_SYS_ALREADY_INIT) { Toast.makeText(mActivity, "hciInit error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); return false; } // 获取授权/更新授权文件 : errCode = checkAuthAndUpdateAuth(); if (errCode != HciErrorCode.HCI_ERR_NONE) { // 由于系统已经初始化成功,在结束前需要调用方法hciRelease()进行系统的反初始化 Toast.makeText(mActivity, "CheckAuthAndUpdateAuth error: " + HciCloudSys.hciGetErrorInfo(errCode), Toast.LENGTH_SHORT).show(); HciCloudSys.hciRelease(); return false; } return true; } /** * 加载初始化信息 * * @return 系统初始化参数 */ private InitParam getInitParam() { String authDirPath = mActivity.getFilesDir().getAbsolutePath(); // 前置条件:无 InitParam initparam = new InitParam(); // 授权文件所在路径,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTH_PATH, authDirPath); // 是否自动访问云授权,详见 获取授权/更新授权文件处注释 initparam.addParam(InitParam.AuthParam.PARAM_KEY_AUTO_CLOUD_AUTH, "no"); // 灵云云服务的接口地址,此项必填 initparam.addParam(InitParam.AuthParam.PARAM_KEY_CLOUD_URL, ConfigUtil.CLOUD_URL); // 开发者Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_DEVELOPER_KEY, ConfigUtil.DEVELOPER_KEY); // 应用Key,此项必填,由捷通华声提供 initparam.addParam(InitParam.AuthParam.PARAM_KEY_APP_KEY, ConfigUtil.APP_KEY); // 配置日志参数 String sdcardState = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(sdcardState)) { String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String packageName = mActivity.getPackageName(); String logPath = sdPath + File.separator + "sinovoice" + File.separator + packageName + File.separator + "log" + File.separator; // 日志文件地址 File fileDir = new File(logPath); if (!fileDir.exists()) { fileDir.mkdirs(); } // 日志的路径,可选,如果不传或者为空则不生成日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_PATH, logPath); // 日志数目,默认保留多少个日志文件,超过则覆盖最旧的日志 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_COUNT, "5"); // 日志大小,默认一个日志文件写多大,单位为K initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_FILE_SIZE, "1024"); // 日志等级,0=无,1=错误,2=警告,3=信息,4=细节,5=调试,SDK将输出小于等于logLevel的日志信息 initparam.addParam(InitParam.LogParam.PARAM_KEY_LOG_LEVEL, "5"); } return initparam; } /** * 获取授权 * * @return 授权结果 */ private int checkAuthAndUpdateAuth() { // 获取系统授权到期时间 int initResult; AuthExpireTime objExpireTime = new AuthExpireTime(); initResult = HciCloudSys.hciGetAuthExpireTime(objExpireTime); if (initResult == HciErrorCode.HCI_ERR_NONE) { // 显示授权日期,如用户不需要关注该值,此处代码可忽略 Date date = new Date(objExpireTime.getExpireTime() * 1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); Log.i(TAG, "expire time: " + sdf.format(date)); if (objExpireTime.getExpireTime() * 1000 > System.currentTimeMillis()) { // 已经成功获取了授权,并且距离授权到期有充足的时间(>7天) Log.i(TAG, "checkAuth success"); return initResult; } } // 获取过期时间失败或者已经过期 initResult = HciCloudSys.hciCheckAuth(); if (initResult == HciErrorCode.HCI_ERR_NONE) { Log.i(TAG, "checkAuth success"); return initResult; } else { Log.e(TAG, "checkAuth failed: " + initResult); return initResult; } } } 使用 初始化 // 灵云语音工具类 HciUtil mInitTts = new HciUtil(this); // 初始化灵云语音 boolean isInitHci = mInitTts.initHci(); if (isInitHci) { // 初始化成功 // 语音合成能力工具类 mTtsUtil = new TtsUtil(this); // 初始化语音合成 isInitPlayer = mTtsUtil.initPlayer(this); } 语音合成 /** * 开始合成 * * @param view v */ public void synth(View view) { if (!isInitPlayer) { Toast.makeText(this, "初始化失败", Toast.LENGTH_SHORT).show(); return; } String text = mEditText.getText().toString(); if (TextUtils.isEmpty(text)) { Toast.makeText(this, "合成内容为空", Toast.LENGTH_SHORT).show(); return; } mTtsUtil.synth(text); } 语音合成回调 // 语音合成的回调 @Override public void onPlayerEventStateChange(TTSCommonPlayer.PlayerEvent playerEvent) { } @Override public void onPlayerEventProgressChange(TTSCommonPlayer.PlayerEvent playerEvent, int i, int i1) { } @Override public void onPlayerEventPlayerError(TTSCommonPlayer.PlayerEvent playerEvent, int i) { } 灵云离线语音合成 离线语音合成相对也比较简单,首先要下载离线资源,下载完以后是一个zip包,解压缩里面有类似这样的几个文件 给每个文件重命名,前面加lib,后面加后缀.so,然后导入工程 导入资源文件以后,修改capKey为离线语音合成 public static final String CAP_KEY = "tts.local.synth"; 注:灵云的离线语音能力,第一次使用的时候也是需要有一个联网授权的过程,授权成功以后,即可在授权期内使用离线语音能力。
效果图 打开WIFI并获取WIFI列表 连接到指定WIFI 直接连接配置过的WIFI 密码错误 源码 KqwWifiManagerDemo WIFI的获取、连接状态等等的信息,都是通过广播回调的. 下面介绍了主要的方法,更多请查看KqwWifiManager 注册广播接收者 <!-- 监听网络状态的广播接收者 --> <receiver android:name=".KqwWifiManager$NetworkBroadcastReceiver"> <intent-filter> <!-- AP扫描完成,客户端得到可用的结果集 --> <action android:name="android.net.wifi.SCAN_RESULTS" /> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> <action android:name="android.net.wifi.WIFI_STATE_CHANGED" /> <action android:name="android.net.wifi.STATE_CHANGE" /> <action android:name="android.net.wifi.supplicant.STATE_CHANGE" /> </intent-filter> </receiver> 广播接收者 /** * 广播接收者 */ public static class NetworkBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { try { String action = intent.getAction(); Log.i(TAG, "onReceive: action = " + action); // 监听WIFI的启用状态 if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0); switch (wifiState) { case WifiManager.WIFI_STATE_ENABLING: Log.i(TAG, "onReceive: 正在打开 WIFI..."); break; case WifiManager.WIFI_STATE_ENABLED: Log.i(TAG, "onReceive: WIFI 已打开"); if (null != mOnWifiEnabledListener) { mOnWifiEnabledListener.onWifiEnabled(true); mOnWifiEnabledListener.onFinish(); } break; case WifiManager.WIFI_STATE_DISABLING: Log.i(TAG, "onReceive: 正在关闭 WIFI..."); break; case WifiManager.WIFI_STATE_DISABLED: Log.i(TAG, "onReceive: WIFI 已关闭"); if (null != mOnWifiEnabledListener) { mOnWifiEnabledListener.onWifiEnabled(false); mOnWifiEnabledListener.onFinish(); } break; case WifiManager.WIFI_STATE_UNKNOWN: Log.i(TAG, "onReceive: WIFI 状态未知!"); break; default: break; } } // WIFI扫描完成 if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) { if (null != mOnWifiScanResultsListener) { mOnWifiScanResultsListener.onScanResults(getScanResults()); mOnWifiScanResultsListener.onFinish(); } } // WIFI 连接状态的监听(只有WIFI可用的时候,监听才会有效) if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); if (null != networkInfo && networkInfo.isConnected()) { WifiInfo wifiInfo = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO); if (null != wifiInfo && String.format("\"%s\"", mConnectingSSID).equals(wifiInfo.getSSID()) && null != mOnWifiConnectListener) { // WIFI连接成功 mOnWifiConnectListener.onSuccess(wifiInfo.getSSID()); mOnWifiConnectListener.onFinish(); mOnWifiConnectListener = null; } } } // WIFI连接过程的监听 if (intent.getAction().equals(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)) { WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); SupplicantState state = wifiInfo.getSupplicantState(); switch (state) { case INTERFACE_DISABLED: // 接口禁用 Log.i(TAG, "onReceive: INTERFACE_DISABLED 接口禁用"); break; case DISCONNECTED:// 断开连接 case INACTIVE: // 不活跃的 Log.i(TAG, "onReceive: INACTIVE 不活跃的 DISCONNECTED:// 断开连接"); if (null != mOnWifiConnectListener) { // 断开当前网络失败 mOnWifiConnectListener.onFailure(); // 连接完成 mOnWifiConnectListener.onFinish(); mOnWifiConnectListener = null; mConnectingSSID = null; } break; case SCANNING: // 正在扫描 Log.i(TAG, "onReceive: SCANNING 正在扫描"); break; case AUTHENTICATING: // 正在验证 Log.i(TAG, "onReceive: AUTHENTICATING: // 正在验证"); if (null != mOnWifiConnectListener) { mOnWifiConnectListener.onConnectingMessage("正在验证"); } break; case ASSOCIATING: // 正在关联 Log.i(TAG, "onReceive: ASSOCIATING: // 正在关联"); if (null != mOnWifiConnectListener) { mOnWifiConnectListener.onConnectingMessage("正在关联"); } break; case ASSOCIATED: // 已经关联 Log.i(TAG, "onReceive: ASSOCIATED: // 已经关联"); if (null != mOnWifiConnectListener) { mOnWifiConnectListener.onConnectingMessage("已经关联"); } break; case FOUR_WAY_HANDSHAKE: Log.i(TAG, "onReceive: FOUR_WAY_HANDSHAKE:"); break; case GROUP_HANDSHAKE: Log.i(TAG, "onReceive: GROUP_HANDSHAKE:"); break; case COMPLETED: // 完成 Log.i(TAG, "onReceive: COMPLETED: // 完成"); if (null != mOnWifiConnectListener) { mOnWifiConnectListener.onConnectingMessage("正在连接..."); } break; case DORMANT: Log.i(TAG, "onReceive: DORMANT:"); break; case UNINITIALIZED: // 未初始化 Log.i(TAG, "onReceive: UNINITIALIZED: // 未初始化"); break; case INVALID: // 无效的 Log.i(TAG, "onReceive: INVALID: // 无效的"); break; default: break; } } } catch (Exception e) { e.printStackTrace(); } } } 权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 打开WIFI /** * 打开Wifi */ public void openWifi(@NonNull OnWifiEnabledListener listener) { if (!mWifiManager.isWifiEnabled()) { mOnWifiEnabledListener = listener; mOnWifiEnabledListener.onStart(true); mWifiManager.setWifiEnabled(true); } } 关闭WIFI /** * 关闭Wifi */ public void closeWifi(@NonNull OnWifiEnabledListener listener) { if (mWifiManager.isWifiEnabled()) { mOnWifiEnabledListener = listener; mOnWifiEnabledListener.onStart(false); mWifiManager.setWifiEnabled(false); } } 扫描附近的WIFI /** * 扫描附近的WIFI * * @param listener 扫描完成的回调接口 */ public void startScan(@NonNull OnWifiScanResultsListener listener) { try { mOnWifiScanResultsListener = listener; mOnWifiScanResultsListener.onStart(); // 先返回缓存 mOnWifiScanResultsListener.onScanResults(getScanResults()); // 重新开始扫描 mWifiManager.startScan(); } catch (Exception e) { e.printStackTrace(); } } 获取WIFI列表 /** * 获取Wifi列表 * * @return Wifi列表 */ private static List<ScanResult> getScanResults() { try { // 得到扫描结果 return mWifiManager.getScanResults(); } catch (Exception e) { e.printStackTrace(); return null; } } 通过密码连接到WIFI /** * 通过密码连接到WIFI * * @param scanResult 要连接的WIFI * @param pwd 密码 * @param listener 连接的监听 */ public void connectionWifiByPassword(@NonNull ScanResult scanResult, @Nullable String pwd, @NonNull OnWifiConnectListener listener) { // SSID String SSID = scanResult.SSID; // 加密方式 SecurityMode securityMode = getSecurityMode(scanResult); // 生成配置文件 WifiConfiguration addConfig = createWifiConfiguration(SSID, pwd, securityMode); int netId; // 判断当前配置是否存在 WifiConfiguration updateConfig = isExists(addConfig); if (null != updateConfig) { // 更新配置 netId = mWifiManager.updateNetwork(updateConfig); } else { // 添加配置 netId = mWifiManager.addNetwork(addConfig); } // 通过NetworkID连接到WIFI connectionWifiByNetworkId(SSID, netId, listener); } 直接连接配置过的WIFI /** * 通过NetworkId连接到WIFI (配置过的网络可以直接获取到NetworkID,从而不用再输入密码) * * @param SSID WIFI名字 * @param networkId NetworkId * @param listener 连接的监听 */ public void connectionWifiByNetworkId(@NonNull String SSID, int networkId, @NonNull OnWifiConnectListener listener) { // 正要连接的SSID mConnectingSSID = SSID; // 连接的回调监听 mOnWifiConnectListener = listener; // 连接开始的回调 mOnWifiConnectListener.onStart(SSID); /* * 判断 NetworkId 是否有效 * -1 表示配置参数不正确 */ if (-1 == networkId) { // 连接WIFI失败 if (null != mOnWifiConnectListener) { // 配置错误 mOnWifiConnectListener.onFailure(); // 连接完成 mOnWifiConnectListener.onFinish(); mOnWifiConnectListener = null; mConnectingSSID = null; } return; } // 获取当前的网络连接 WifiInfo wifiInfo = getConnectionInfo(); if (null != wifiInfo) { // 断开当前连接 boolean isDisconnect = disconnectWifi(wifiInfo.getNetworkId()); if (!isDisconnect) { // 断开当前网络失败 if (null != mOnWifiConnectListener) { // 断开当前网络失败 mOnWifiConnectListener.onFailure(); // 连接完成 mOnWifiConnectListener.onFinish(); mOnWifiConnectListener = null; mConnectingSSID = null; } return; } } // 连接WIFI boolean isEnable = mWifiManager.enableNetwork(networkId, true); if (!isEnable) { // 连接失败 if (null != mOnWifiConnectListener) { // 连接失败 mOnWifiConnectListener.onFailure(); // 连接完成 mOnWifiConnectListener.onFinish(); mOnWifiConnectListener = null; mConnectingSSID = null; } } } 断开指定WIFI /** * 断开WIFI * * @param netId netId * @return 是否断开 */ public boolean disconnectWifi(int netId) { boolean isDisable = mWifiManager.disableNetwork(netId); boolean isDisconnect = mWifiManager.disconnect(); return isDisable && isDisconnect; } 删除配置 /** * 删除配置 * * @param netId netId * @return 是否删除成功 */ private boolean deleteConfig(int netId) { boolean isDisable = mWifiManager.disableNetwork(netId); boolean isRemove = mWifiManager.removeNetwork(netId); boolean isSave = mWifiManager.saveConfiguration(); return isDisable && isRemove && isSave; } 生成配置信息 /** * 生成新的配置信息 用于连接Wifi * * @param SSID WIFI名字 * @param password WIFI密码 * @param mode WIFI加密类型 * @return 配置 */ private WifiConfiguration createWifiConfiguration(@NonNull String SSID, @Nullable String password, @NonNull SecurityMode mode) { WifiConfiguration config = new WifiConfiguration(); config.allowedAuthAlgorithms.clear(); config.allowedGroupCiphers.clear(); config.allowedKeyManagement.clear(); config.allowedPairwiseCiphers.clear(); config.allowedProtocols.clear(); config.SSID = "\"" + SSID + "\""; if (mode == SecurityMode.OPEN) { //WIFICIPHER_NOPASS // config.wepKeys[0] = ""; config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); // config.wepTxKeyIndex = 0; } else if (mode == SecurityMode.WEP) { //WIFICIPHER_WEP config.hiddenSSID = true; config.wepKeys[0] = "\"" + password + "\""; config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104); config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); config.wepTxKeyIndex = 0; } else if (mode == SecurityMode.WPA) { //WIFICIPHER_WPA config.preSharedKey = "\"" + password + "\""; config.hiddenSSID = true; config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); //config.allowedProtocols.set(WifiConfiguration.Protocol.WPA); config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); config.status = WifiConfiguration.Status.ENABLED; } return config; } 目前存在的问题 目前存在的问题是,当删除配置以后,再连接WIFI,调用addNetwork或者updateNetwork都会返回-1,需要重启一下WIFI才会正常,有知道原因还请指点一下。
Android蓝牙通信 效果图 两台真机设备 源码 GitHub 关于蓝牙的开关控制,设置设备可见、搜索附近的蓝牙设备,已经封装到了 BluetoothManager 类 关于设备的连接、通信。已经封装到了 BluetoothService 类 注:下面的全部内容,主要是思路,具体的可以参考上面的源码,如果对你有帮助记得给个赞哦。 权限 <!-- 蓝牙的权限 --> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> 蓝牙的打开与关闭 开启蓝牙 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); /** * 开启蓝牙 */ public void openBluetooth() { try { mBluetoothAdapter.enable(); } catch (Exception e) { e.printStackTrace(); } } 关闭蓝牙 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); /** * 关闭蓝牙 */ public void closeBluetooth() { try { mBluetoothAdapter.disable(); } catch (Exception e) { e.printStackTrace(); } } 设置蓝牙设备可见 设置设备可见对于服务端是必须的,客户端设不设置无所谓。 如果服务端不可见,配对过的设备也搜索到并可以连接上,但是不能通信,没有配对过的设备连搜索都搜索不到。 可见时间的取值范围是0到120,单位是秒,0表示永久可见。 手机上的蓝牙可见仅限一次连接有效。也就是说,一次连接断开以后,下次再等待客户端连接的时候,还需要再设置一次设备可见。 /** * 设置设备可见 * 0 ~ 120 */ public void setDuration() { Intent duration = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); duration.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120); mActivity.startActivity(duration); } 扫描附近的蓝牙设备 扫描附近设备的设备,需要注册一个广播接收者,来接收扫描到的结果。 需要注意的是,接收扫描结果的广播接收者必须使用动态注册,不能在清单文件里注册! 注册搜索蓝牙设备的广播接收者 // 获取设备的广播接收者 FoundDeviceBroadcastReceiver mFoundDeviceBroadcastReceiver = new FoundDeviceBroadcastReceiver(); // 注册receiver监听 IntentFilter mFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND); /** * 注册搜索蓝牙设备的广播接收者 */ public void registerFoundDeviceReceiver() { mActivity.registerReceiver(mFoundDeviceBroadcastReceiver, mFilter); } 反注册搜索蓝牙设备的广播接收者 // 获取设备的广播接收者 FoundDeviceBroadcastReceiver mFoundDeviceBroadcastReceiver = new FoundDeviceBroadcastReceiver(); /** * 反注册搜索蓝牙设备的广播接收者 */ public void unregisterReceiver() { mActivity.unregisterReceiver(mFoundDeviceBroadcastReceiver); } 广播接收者 /** * Created by kqw on 2016/8/2. * 蓝牙的广播接收者 */ public class FoundDeviceBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "FoundDeviceBroadcast"; private static OnFoundDeviceListener mOnFoundDeviceListener; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // 获取设备 BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (BluetoothDevice.ACTION_FOUND.equals(action)) { // 扫描发现的设备 if (null != mOnFoundDeviceListener) { mOnFoundDeviceListener.foundDevice(btDevice); } } …… } public void setOnFoundDeviceListener(OnFoundDeviceListener listener) { mOnFoundDeviceListener = listener; } } 开始扫描附近的蓝牙设备 /** * 开始扫描设备 */ public void startDiscovery() { Log.i(TAG, "startDiscovery: "); if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } else { // TODO 这里可以先获取已经配对的设备 // Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); // 开始扫描设备 mBluetoothAdapter.startDiscovery(); } } 获取已经配对的设备 扫描附近的蓝牙设备是一个很消耗性能的操作,在扫描之前,可以先获取已经配对过的设备,如果已经配对过,就不用再扫描了。 BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); 蓝牙连接 获取到附近的设备以后,就可以通过MAC地址进行配对连接了。 配对 没有配对过的设备,在连接之前是需要配对的,配对成功才可以连接、通信。 配对可以手动点击,根据配对码进行配对,也可以设置自动配对。手动配对没什么好说的,这里介绍自动配对 还是用到上面的蓝牙广播接收者,我们在清单文件里添加Action <!-- 蓝牙广播接收者 --> <receiver android:name=".receiver.FoundDeviceBroadcastReceiver"> <intent-filter> <!-- 添加配对请求 --> <action android:name="android.bluetooth.device.action.PAIRING_REQUEST" /> </intent-filter> </receiver> 注意:这里的自动配对仅支持4.4.2以上系统,以下的版本想要实现需要用到反射 /** * Created by kqw on 2016/8/2. * 蓝牙的广播接收者 */ public class FoundDeviceBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "FoundDeviceBroadcast"; private static OnFoundDeviceListener mOnFoundDeviceListener; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // 获取设备 BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(action)) { if (new ConfigUtil(context).getPairingConfirmation()) { // 收到配对请求 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // 同意请求 btDevice.setPairingConfirmation(true); } else { Log.i(TAG, "onReceive: 4.4.2 以下版本的设备需要通过反射实现自动配对"); } } } …… } } 连接 服务端等待连接 服务端开启连接,需要开启一个阻塞线程,等待客户端的连接,类似这样 try { // 等待客户端连接 阻塞线程 连接成功继续向下执行 连接失败抛异常 socket = mmServerSocket.accept(); } catch (IOException e) { Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e); break; } 客户端发起连接 客户端发起连接,如果没有配对过,需要先进行配对,连接同样是一个阻塞线程,连接成功会继续向下执行,连接失败会抛异常,类似这样 try { …… // 开始连接 阻塞线程 连接成功继续执行 连接失败抛异常 mmSocket.connect(); } catch (IOException e) { // 连接失败 e.printStackTrace(); try { mmSocket.close(); } catch (IOException e2) { Log.e(TAG, "unable to close() " + mSocketType + " socket during connection failure", e2); } …… } 通信 接收数据 连接成功以后,需要开启一个线程,一直循环读取数据流,类似这样 // 只有蓝牙处于连接状态就一直循环读取数据 while (mState == STATE_CONNECTED) { try { // Read from the InputStream bytes = mmInStream.read(buffer); // 读取到数据的回调 …… } catch (IOException e) { // 读取数据出现异常 Log.e(TAG, "disconnected", e); …… } } 发送数据 /** * 发数据 * * @param buffer 发送内容 */ public void write(final byte[] buffer) { try { mmOutStream.write(buffer); // 发送数据的回调 …… } catch (IOException e) { // 发送数据出现失败 Log.e(TAG, "Exception during write", e); } } 坑 有时候当你重复的连接、断开、连接、断开…… 你会发现出现连接失败,或者可以连接成功,但是不能通信了,这个时候你要考虑你得服务端是不是已经不可见了。 上面已经提过,如果两个设备已经配对,即使服务端是不可见的,也同样可以搜索到并连接上,但是是不能通信的。 而如果两个设备没有配对过,是连搜索都搜索不到服务端的。 当然,如果在服务端可见的时候,连接上就是连接上了,只要连接不断开,即使连接上以后服务端变为不可见了,也一样可以一直通信。
Android设备的唯一标识 IMEI 权限 <uses-permission android:name="android.permission.READ_PHONE_STATE" /> 获取IMEI /** * 获取IMEI * * @return IMEI */ private String getIMEI() { try { TelephonyManager TelephonyMgr = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); return TelephonyMgr.getDeviceId(); } catch (Exception e) { e.printStackTrace(); return null; } } WLAN MAC Address 权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> 获取MAC地址 /** * 获取WLAN MAC Address * * @return MAC地址 */ private String getWLANMacAddress() { try { WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); return wifiInfo.getMacAddress(); } catch (Exception e) { e.printStackTrace(); return null; } } BT MAC Address 权限 <uses-permission android:name="android.permission.BLUETOOTH" /> 获取MAC地址 /** * 获取BT MAC Address * * @return MAC地址 */ private String getBTMacAddress() { try { BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); return bluetoothAdapter.getAddress(); } catch (Exception e) { e.printStackTrace(); return null; } } MD5加密 附加一个MD5的加密算法,考虑在某些特殊设备上可能获取不到某个ID,可以获取多个ID,组合起来,通过MD5算法,得到一个32位的唯一标识 /** * MD5加密 * * @param text 要加密的字符串 * @return 加密后的32位结果 */ private String digest(String text) { try { // 创建一个MD5加密算法 MessageDigest digest = MessageDigest.getInstance("MD5"); // 创建StringBuffer保存十六进制数据 StringBuffer sb = new StringBuffer(); // 换成字节数组 byte[] bytes = digest.digest(text.getBytes()); // 遍历字节数组加密 for (byte bt : bytes) { // 将负数转换为正数 int i = bt & 0xff; // 将十进制转换为十六进制 String hex = Integer.toHexString(i); // 如果不够2位,前面补0 if (2 > hex.length()) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } 例: String text = getWLANMacAddress() + getBTMacAddress() + getIMEI(); String md5 = digest(text); Toast.makeText(this, "MD5:" + md5, Toast.LENGTH_SHORT).show();
Android内存和SD卡的数据存取 内存 向内存存取数据,不需要任何权限 效果图 存 private String fileName = "test.txt"; public void saveToROM(View view) { File file = new File(getFilesDir(), fileName); try { FileOutputStream fos = new FileOutputStream(file); String str = "这是我写入内存的数据"; fos.write(str.getBytes()); fos.close(); Toast.makeText(this, "保存到内存成功", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, "保存到内存失败", Toast.LENGTH_SHORT).show(); } } 取 private String fileName = "test.txt"; public void readFromROM(View view) { File file = new File(getFilesDir(), fileName); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); String text = br.readLine(); br.close(); Toast.makeText(this, "读取:\n" + text, Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); } } 保存成功以后,可以在data/data/包名/files下看到文件 SD卡 保存到SD卡和保存到内存基本一样,只不过存储路径不一样,需要SD的存取权限,另外需要注意的是有的设备可能没有SD卡。 权限 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 存 private String fileName = "test.txt"; public void saveToSD(View view) { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // sd卡可用 File file = new File(Environment.getExternalStorageDirectory(), fileName); try { FileOutputStream fos = new FileOutputStream(file); String str = "这是我写入SD卡的数据"; fos.write(str.getBytes()); fos.close(); Toast.makeText(this, "保存到SD卡成功", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, "保存到SD卡失败", Toast.LENGTH_SHORT).show(); } } else { // 当前不可用 Toast.makeText(this, "SD卡不可用", Toast.LENGTH_SHORT).show(); } } 取 private String fileName = "test.txt"; public void readFromSD(View view) { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // SD卡可用 File file = new File(Environment.getExternalStorageDirectory(), fileName); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); String text = br.readLine(); br.close(); Toast.makeText(this, "读取:\n" + text, Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); } } else { // SD卡不可用 Toast.makeText(this, "SD卡不可用", Toast.LENGTH_SHORT).show(); } }
Android使用OpenCV实现「人脸检测」和「人脸识别」 DEMO OpenCV+JavaCV实现人脸识别 —————————-分割线——————————— 效果图 先上效果图,不好弄gif 在网上找了在Android平台上使用OpenCV相关的教程,很少,大部分也都主要是介绍下人脸检测,很少有讲人脸识别,还有的人连人脸检测和人脸识别的概念都没有搞清,人脸检测只是识别到有人脸,能获取到一个人脸的大概位置,有几个人脸,而人脸识别是要获取到人脸特征做对比,识别这个人脸。有好多文章都写自己在讲人脸识别,实际上他只是做了人脸检测。 OpenCV官网 官方给的Demo是在Eclipse工程下的,如果你现在已经是在Android Studio下开发,因为Eclipse工程有makefile文件,迁移到Android Studio好像还是有点麻烦,我是干脆就在Eclipse下跑的Demo。 先甩过来官方给的一些文档: OpenCV4Android SDK Android Development with OpenCV 实现方式 按照官方的文档,我们在Eclipse里导入Demo进去以后,是不能直接运行的,需要安装Manager的一个APK,然后在Demo工程里通过AIDL的方式,调用OpenCV的核心方法,不过Demo给实现的功能也只是一个人脸检测。 SDK SDK下载 下面来看一下SDK 目录: apk:Manager的apk doc:一些文档 samples:示例工程和一些编译好的apk sdk:一些库文件 当然, 如果你的C/C++足够好,你肯定可以自己编译一个库,直接导入到工程,就不用安装Manager了,可惜了我自己还不行,哈哈……无奈安装Manager把…… 如何将Demo导入到Eclipse并运行,上面官方的文档已经说的比较清楚了,至于会有什么问题就自行Google吧。 人脸检测 其实人脸检测并不是重点,Demo里已经实现了人脸检测的功能。 主要的实现方式:OpenCV有一个自己的org.opencv.android.JavaCameraView自定义控件,它循环的从摄像头抓取数据,在回调方法中,我们能获取到Mat数据,然后通过调用OpenCV的Native方法,检测当前是否有人脸,我们会获取到一个Rect数组,里面会有人脸数据,最后将人脸画在屏幕上,到此为止,Demo的人脸检测功能,就结束了。 人脸识别 人脸识别我这里用到了JavaCV 人脸识别逻辑:人脸识别的主要方式就是获取到人脸的特征值,然后将两个特征值做比对,取到一个相似度去做人脸识别,OpenCV这里的特征值,其实就是一张图片。 我们的从回调的Mat数据检测到有人脸以后,提取特征值(也就是保存人脸的一张图片到某个路径),然后比较特征值 为了提高识别的准确度,需要在检测到人脸以后,把人脸的部分截取出来,然后置灰(置灰的目的是为了方式色泽和明暗度对识别有影响)。 保存人脸特征值 /** * 特征保存 * * @param image Mat * @param rect 人脸信息 * @param fileName 文件名字 * @return 保存是否成功 */ public boolean saveImage(Mat image, Rect rect, String fileName) { try { String PATH = Environment.getExternalStorageDirectory() + "/FaceDetect/" + fileName + ".jpg"; // 把检测到的人脸重新定义大小后保存成文件 Mat sub = image.submat(rect); Mat mat = new Mat(); Size size = new Size(100, 100); Imgproc.resize(sub, mat, size); Highgui.imwrite(PATH, mat); return true; } catch (Exception e) { e.printStackTrace(); return false; } } 提取特征值 之前已经说了嘛,人脸特征其实就是一张人脸图片,所以提取人脸特征其实就是获取一张人脸图片 /** * 提取特征 * * @param fileName 文件名 * @return 特征图片 */ public Bitmap getImage(String fileName) { try { return BitmapFactory.decodeFile(Environment.getExternalStorageDirectory() + "/FaceDetect/" + fileName + ".jpg"); } catch (Exception e) { e.printStackTrace(); return null; } } 人脸识别 这里主要使用了JavaCV的方法 /** * 特征对比 * * @param file1 人脸特征 * @param file2 人脸特征 * @return 相似度 */ public double CmpPic(String file1, String file2) { int l_bins = 20; int hist_size[] = {l_bins}; float v_ranges[] = {0, 100}; float ranges[][] = {v_ranges}; opencv_core.IplImage Image1 = cvLoadImage(Environment.getExternalStorageDirectory() + "/FaceDetect/" + file1 + ".jpg", CV_LOAD_IMAGE_GRAYSCALE); opencv_core.IplImage Image2 = cvLoadImage(Environment.getExternalStorageDirectory() + "/FaceDetect/" + file2 + ".jpg", CV_LOAD_IMAGE_GRAYSCALE); opencv_core.IplImage imageArr1[] = {Image1}; opencv_core.IplImage imageArr2[] = {Image2}; opencv_imgproc.CvHistogram Histogram1 = opencv_imgproc.CvHistogram.create(1, hist_size, CV_HIST_ARRAY, ranges, 1); opencv_imgproc.CvHistogram Histogram2 = opencv_imgproc.CvHistogram.create(1, hist_size, CV_HIST_ARRAY, ranges, 1); cvCalcHist(imageArr1, Histogram1, 0, null); cvCalcHist(imageArr2, Histogram2, 0, null); cvNormalizeHist(Histogram1, 100.0); cvNormalizeHist(Histogram2, 100.0); return cvCompareHist(Histogram1, Histogram2, CV_COMP_CORREL); } 最后 看我轻描淡写了一篇博客写完了,看上去好像挺容易的,但是对于第一次做人脸识别的人或者对JNI还不是很熟的人来说,可能并没有想象的那么简单,你会遇到各种库找不到,编译不通过等等的问题,总之吧,一个大概的实现思路呈现出来,仅供参考,就是这样!
图灵机器人SDK接入指南 图灵机器人:官网 注册账号 注册个账号,创建个应用,进入后台,点开机器人接入,你会看到API key和secret 下载SDK 点击下载Android SDK,解压以后会有libs copy到自己的工程 加入权限 如果用API接入,应该只用网络权限就行了把,官方要求下面这几个权限,时间紧迫,我也没有一一去验证,直接加上把。 <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> 添加jar 将下载的SDK里的libs目录下的jar和so都导入工程 想不明白,为什么Demo还在使用Eclipse,是Demo没有更新,还是工程师…… 图灵SDK初始化 // turingSDK初始化 SDKInitBuilder builder = new SDKInitBuilder(this).setSecret(`这里填写secret`).setTuringKey(`这里填写API Key`).setUniqueId(`这里填写自己添加的一个标示符,如邮箱、 手机号等等`); SDKInit.init(builder, new InitListener() { @Override public void onFail(String error) { Log.d(TAG, error); } @Override public void onComplete() { // 获取userid成功后,才可以请求Turing服务器,需要请求必须在此回调成功,才可正确请求 mTuringApiManager = new TuringApiManager(MainActivity.this); mTuringApiManager.setHttpListener(myHttpConnectionListener); } }); 图灵网络请求回调 /** * 网络请求回调 */ HttpConnectionListener myHttpConnectionListener = new HttpConnectionListener() { @Override public void onSuccess(RequestResult result) { if (result != null) { try { Log.d(TAG, result.getContent().toString()); mTv2.setText(result.getContent().toString()); JSONObject result_obj = new JSONObject(result.getContent().toString()); if (result_obj.has("text")) { Log.d(TAG, result_obj.get("text").toString()); // 科大讯飞语音合成 mBitMainSpeechCompound.speaking(result_obj.get("text").toString()); } } catch (JSONException e) { Log.d(TAG, "JSONException:" + e.getMessage()); } } } @Override public void onError(ErrorMessage errorMessage) { Log.d(TAG, errorMessage.getMessage()); } }; 图灵解析 mTuringApiManager.requestTuringAPI(data); Demo Demo只是简单的使用了下图灵SDK,没有使用任何设计模式,可以作为参考 GitHub:TuringSDK
Android创建桌面快捷方式 效果图 添加权限 <!-- 添加创建快捷方式的权限 --> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> 配置快捷启动的Activity 在清单文件下,将要设置快捷启动的Activity添加intent-filter属性 AndroidManifest.xml <activity android:name=".Main2Activity"> <intent-filter> <action android:name="android.intent.action.home" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> 创建快捷方式 /** * 创建快捷图标 * * @param view view */ public void createShortcutIcon(View view) { Toast.makeText(this, "创建快捷图标", Toast.LENGTH_SHORT).show(); String title = "第二页"; Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); Intent intent = new Intent(this, Main2Activity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 添加Intent Intent createShortcutIconIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); // 标题 createShortcutIconIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); // 图标 createShortcutIconIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); // Intent createShortcutIconIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent); // 发送广播创建图标 sendBroadcast(createShortcutIconIntent); }
原文链接 我自己觉着有用,所以转载了一下,format的规则我肯定不会刻意去记,用到的时候想不起来查看一下就可以了 小伙伴可以看下面长图 如果看不清,可以 在图片上鼠标右键->在新标签中打开图片 然后就可以放大查看拉