孔庆威的博客

精于心,简于形


  • 首页

  • 分类

  • 归档

  • 标签

Gradle常用技巧

发表于 2017-03-02 |

转载请说明出处!
作者: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,分别添加了三种类型的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 下看到我们添加的参数

P1

因为是静态变量,取值时直接用类名点变量名即可

P2

上述属于在Java代码中添加字段,同样的,Gradle也支持添加xml属性,类似这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 下生产如下字段

P3

但是要避免和string.xml文件里的字段重复

在xml中使用

1
android:text="@string/coder"

或者再Java中使用

1
String coder = getString(R.string.coder);

添加工程build时间

有时候,测试和产品总是会拿着手机跑过来问你,“有没有更新?”、“帮我看一下我装的是不是最新的版本?”,总是很烦,我们可以利用Gradle,获取到工程的Build时间,在测试版本打印出来,可以为工程师节省不少时间。

Gradle支持直接添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 {
……
}

判断的时候调用

1
BuildConfig.buildTime

或者

1
getString(R.string.build_time)

P4

不同版本包名不同

我们都知道,相同包名,不同签名,在一台手机上是无法同时安装的。前面我们配置了两个不同的版本,release版本肯定是用正式签名,debug版本我们通常都会使用测试签名。那么问题来了,测试人员如果已经在手机上安装了上线的版本,再装测试版,就会有冲突,必须要卸载一个,那么最好的办法就是修改测试版包名。这样包名不同,两个版本同时安装,也不会有冲突。

同样的,Gradle依然可以轻松的帮我们做到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apply plugin: 'com.android.application'
android {
……
defaultConfig {
……
}
buildTypes {
release {
……
}
debug {
……
applicationIdSuffix ".debug"
}
}
}
dependencies {
……
}

可以看到我们在 debug 版本添加了applicationIdSuffix,其值为 .debug,顾名思义,是debug版本在包名后面添加.debug后缀。

多渠道打包

多渠道打包,打包多个市场的apk,用来统计。

首先在AndroidManifest文件添加meta-data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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两个渠道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apply plugin: 'com.android.application'
android {
……
defaultConfig {
……
}
buildTypes {
release {
……
}
debug {
……
}
}
productFlavors {
xiaomi {
manifestPlaceholders = [CHANNEL_VALUE: "XIAO_MI"]
}
googlePlay {
manifestPlaceholders = [CHANNEL_VALUE: "GOOGLE_PLAY"]
}
}
}
dependencies {
……
}

获取渠道

1
2
3
4
5
6
7
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();
}

打造简洁高效的动态权限管理器

发表于 2017-02-16 | 分类于 开源 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


效果图

这里写图片描述

GitHub地址:PermissionsManager

随着Android 6.0的普及,动态权限的重要性也开始时慢慢体现出来。为了更好的保护用户隐私,Android 6.0要求在进行敏感操作之前,必须要向用户请示申请权限。

如何使用,在之前的文章里也已经介绍过了,但是用起来比较麻烦。Android6.0动态获取权限

我希望可以封装一下,使用之前创建一个动态权限的管理对象,他有两个回调来告诉我权限申请成功或者失败,像这样:

1
2
3
4
5
6
7
8
9
10
11
mPermissionsManager = new PermissionsManager(this) {
@Override
public void authorized(int requestCode) {
// TODO 权限通过
}
@Override
public void noAuthorization(int requestCode, String[] lacksPermissions) {
// TODO 有权限没有通过
}
};

使用的时候,可以直接调用一个方法,把要请示的权限传进去就可以进行校验,像这样:

1
2
// 检查权限
mPermissionsManager.checkPermissions("请求码", "要校验的权限");

于是乎,下面封装的动态权限管理器就来了:

动态权限管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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. 初始化权限管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 初始化
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. 检查权限

1
2
3
4
// 要校验的权限
String[] PERMISSIONS = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
// 检查权限
mPermissionsManager.checkPermissions(0, PERMISSIONS);

3. 复查权限

用户对权限申请的提示做出选择以后,要重写TargetActivity的onRequestPermissionsResult方法来复查权限,检查权限是否通过。

1
2
3
4
5
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// 用户做出选择以后复查权限,判断是否通过了权限申请
mPermissionsManager.recheckPermissions(requestCode, permissions, grantResults);
}

进入应用设置页面

最后,权限没有通过,是不能使用的,如果一定要用,一般要提示用户缺少权限,到应用设置页面去吧权限打开,再回来使用。
对话框就不写了,进入到应用的设置页面可以直接调用PermissionsManager里的startAppSettings方法,进入到该应用的设置页面,修改权限

1
PermissionsManager.startAppSettings(getApplicationContext());

Android自定义View绘图基础

发表于 2016-12-13 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


Android自定义View绘图基础

View的测量

控件的测量可以说是固定写法,原生的View只支持EXACTLY的测量模式,我们自定义的控件可以重写onMeasure方法

1
2
3
4
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getMeasuredSize(widthMeasureSpec), getMeasuredSize(heightMeasureSpec));
}

onMeasure方法给我们返回的widthMeasureSpec和heightMeasureSpec,我们并不能直接拿过来使用,需要使用MeasureSpec类进行解析,来获取测量后的具体值。
首先需要获取测量模式

1
2
3
4
5
6
7
8
9
10
11
12
MeasureSpec.getMode(measureSpec)
```
getMode方法返回是测量的模式,有以下3种类型:
- MeasureSpec.EXACTLY : 精确值模式(指定值/match_parent)
- MeasureSpec.AT_MOST : 最大值模式(wrap_content)
- MeasureSpec.UNSPECIFIED : 不指定大小的测量模式(一般用不上)
获取到了测量模式以后,获取测量后的大小
``` java
MeasureSpec.getSize(measureSpec)

根据上面的意思,可以封装我们的getMeasuredSize方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 默认大小
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的绘制

画笔属性

创建画笔

1
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

点

1
2
3
4
/**
* Helper for drawPoints() for drawing a single point.
*/
public void drawPoint(float x, float y, @NonNull Paint paint)
1
canvas.drawPoint(10, 10, paint);

直线

绘制一条直线

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 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)

绘制多条直线

1
public void drawLines(@Size(multiple=4) @NonNull float[] pts, @NonNull Paint paint)

矩形

1
2
3
4
5
6
7
8
9
10
11
/**
* 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)
1
canvas.drawRect(100, 100, 200, 200, paint);

这里写图片描述

圆角矩形

1
2
3
4
5
6
7
8
9
/**
* 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)
1
canvas.drawRoundRect(100, 100, 200, 200, 20, 20, paint);

这里写图片描述

圆

1
2
3
4
5
6
7
8
9
10
11
/**
* 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)
1
2
// 画圆
canvas.drawCircle(200, 200, 100, paint);

这里写图片描述

扇形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* <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)
1
2
// 扇形 起始角度0度 旋转角度120度
canvas.drawArc(100, 100, 200, 200, 0, 120, true, paint);

这里写图片描述

弧形

扇形通过调整属性,可以实现弧形的效果,首先画笔设置成空心

1
2
// 空心
paint.setStyle(Paint.Style.STROKE);

设置画扇形的useCenter参数为false

1
2
// 弧形 起始角度0度 旋转角度120度
canvas.drawArc(100, 100, 200, 200, 0, 120, false, paint);

这里写图片描述

椭圆

1
2
3
4
5
/**
* 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)
1
2
// 椭圆
canvas.drawOval(100, 100, 500, 300, paint);

这里写图片描述

文字

1
2
3
4
5
6
7
8
9
10
/**
* 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)
1
2
3
4
// 文字
paint.setTextSize(30);
paint.setColor(Color.BLACK);
canvas.drawText("KongQingwei", 100, 100, paint);

这里写图片描述

在指定位置显示每个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
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);

这里写图片描述

绘制路径

可以自定义画出想要的任意图形

五角星

1
2
3
4
5
6
7
8
/**
* 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)
1
2
3
4
5
6
7
8
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);

这里写图片描述

空心

1
paint.setStyle(Paint.Style.STROKE);

这里写图片描述

图形裁剪

以上面的实心五角星为例,以五角星的中心为圆心,裁剪出一个半径为100的圆形

1
2
3
4
5
6
7
8
/**
* 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 裁剪一个圆形
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 外部 这里写图片描述

Android与Javascript交互

发表于 2016-11-16 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


本篇参考Android与HTML+JS交互入门

效果图

效果图

加载本地Html

1
2
3
contentWebView = (WebView) findViewById(R.id.webview);
// 加载Assets下的Html
contentWebView.loadUrl("file:///android_asset/html/test.html");

启用Javascript

1
2
contentWebView.getSettings().setJavaScriptEnabled(true);
contentWebView.addJavascriptInterface(this, "android");

Android调用Javascript的方法

Javascript写法

1
2
3
4
5
6
7
8
<script type="text/javascript">
function jsFunction(){
document.getElementById("content").innerHTML = "JS方法被调用";
}
function jsFunctionArg(arg){
document.getElementById("content").innerHTML = "JS方法被调用并收到参数:<br/>" + arg;
}
</script>

Android写法

1
2
3
4
// 调用JS的jsFunction方法
contentWebView.loadUrl("javascript:jsFunction()");
// 调用JS的jsFunctionArg方法
contentWebView.loadUrl("javascript:jsFunctionArg('[Android传递过来的数据]')");

Javascript调用Android的方法

Android方法

1
2
3
4
@JavascriptInterface
public void androidFunction() {
Snackbar.make(contentWebView, "Android的方法被调用", Snackbar.LENGTH_SHORT).show();
}
1
2
3
4
@JavascriptInterface
public void androidFunction(String text) {
Snackbar.make(contentWebView, "Android的方法被调用并收到参数 : \n" + text, Snackbar.LENGTH_SHORT).show();
}

Javascript调用

1
<input type="button" style="width:300px;height:50px;" value="JS调用Android方法" onclick="window.android.androidFunction()" />
1
<input type="button" style="width:300px;height:50px;" value="JS调用Android带有参数的方法" onclick="window.android.androidFunction('[JS传递过来的数据]')" />

Code

HTML

XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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);
}
}

Android播放音效

发表于 2016-11-16 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


有些时候程序需要播放几个很短的低延迟的音效来响应与用户的交互。

Android通过SoundPool将文件音频缓存加载到内存中,然后在响应用户操作的时候快速地播放。
Android框架低通了SoundPool来解码小音频文件,并在内存中操作它们来进行音频快速和重复的播放。SoundPool还有一些其他特性,比如可以在运行时控制音量和播放速度。

播放音效也很简单,总共分5步

准备音频文件

将音频文件放置在assets目录下

准备音频文件

初始化SoundPool

1
SoundPool mSoundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);

加载音频文件

1
int streamID = mSoundPool.load(getApplicationContext().getAssets().openFd("beep/beep1.mp3"), 1);

播放音频文件

1
mSoundPool.play(streamID, 10, 10, 1, 0, 1.0f);

释放SoundPool

1
2
mSoundPool.release();
mSoundPool = null;

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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;
}
}
}

App启动优化最佳实践

发表于 2016-11-14 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


看了医生写的文章一触即发——App启动优化最佳实践,收获是有的。

做Android开发,一定写给过启动页,在这里做一些初始化的操作,还有就是显示推广信息。

很普通的一个页面,以前测试也给我提出过bug,应用在启动的时候,有时候有白屏/黑屏。当时能做的就是尽量较少耗时操作,上面医生的文章里也有提到,但是通过主题的方式优化这个问题之前还真是不知道的。

下面主要总结一下通过主题的方式优化启动页(医生还提到了在子线程初始化和使用IntentService初始化,都是属于异步初始化,还有延迟初始化,就不说了)

效果图

优化前

优化后

通过修改主题优化启动时白屏/黑屏

原理请移步到医生的文章,我就不复述了,之所以会看到白屏或者黑屏,是和我们的主题有关系的,因为系统默认使用的主题,背景色就是白色/黑色。那么我们自定义一个主题,让默认的样式就是我们想要的,就优化了白屏/黑屏的问题。

首先,我们自定义一个主题,设置一个我们想要的背景

1
2
3
4
<!-- 启动页主题 -->
<style name="SplashTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/start_window</item>
</style>

自定义背景start_window.xml

1
2
3
4
5
6
7
8
9
10
11
<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>

最后,在清单文件设置启动页使用我们自定义的主题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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>

到此大功告成,为了体现出效果,在启动页加载之前,我们模拟一个白屏/黑屏的延时操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
}

Android串口通信

发表于 2016-11-01 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


串口通信偏向嵌入式一点,是Android设备通过串口与其他设备进行通信的一种方式,本文介绍的Android纯串口的通信,并不是手机上的USB串口通信。

P1

手机上是没有这个串口的哦。

关于串口通信,Google已经给出了源码,地址在GitHub android-serialport-api

四年前的代码,还是Eclipse工程,本文主要介绍如何在Android Studio中使用。

源码地址在 KqwSerialPortDemo

集成

Java层的代码,Google已经给封装在 SerialPort.java

导入.so

没有什么难度了,将so导入到项目

P1

导入jni文件

在main目录下创建cpp文件夹,并将jni源文件和CMakeLists.txt导入

P2

在build.gradle配置cmake路径。

1
2
3
4
5
6
7
8
9
android {
……
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
……
}

修改jni源文件

这里要注意jni文件函数名的写法:Java_包名_类名_方法名

P3

在将源码里的jni导入过来的时候,包名是源码Demo的包名,我们在自己的工程里要换成自己的包名、类名,源文件和头文件都要记得改。

修改CMakeLists.txt与SerialPort.java

CMakeLists.txt

1
2
3
4
5
6
7
8
9
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

1
2
3
4
static {
System.loadLibrary("SerialPort");
System.loadLibrary("serial_port");
}

使用

基类

需要使用串口通信的类继承 SerialPortActivity.java

打开串口

  • 端口号:/dev/ttyS2
  • 比特率:115200
1
2
3
4
5
6
public SerialPort getSerialPort() throws SecurityException, IOException, InvalidParameterException {
if (mSerialPort == null) {
mSerialPort = new SerialPort(new File("/dev/ttyS2"), 115200, 0);
}
return mSerialPort;
}

关闭串口

1
2
3
4
5
6
public void closeSerialPort() {
if (mSerialPort != null) {
mSerialPort.close();
mSerialPort = null;
}
}

发送数据

1
2
3
Message message = Message.obtain();
message.obj = text.getBytes();
sendingHandler.sendMessage(message);

接收消息

1
2
3
4
5
6
7
8
9
@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

下载并安装NDK与CMake

OpenCV+JavaCV实现人脸识别

发表于 2016-09-09 | 分类于 开源 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


OpenCV主要实现人脸检测功能

JavaCV主要实现人脸对比功能

具体的就不啰嗦了,本来最近很忙,主要是因为好多人私信我要 Android使用OpenCV实现「人脸检测」和「人脸识别」 的Demo,今天特意抽出时间写了一下。

效果图

效果图

效果图

源码

KqwFaceDetectionDemo

感觉有用的话,就给个star吧,谢谢!!

注意

最后啰嗦一点,如果你的程序是跑在手机、pad等设备上,一般没有什么问题。
但是如果你是在自己的开发板上跑,可能会有一些小插曲。

比如我司的机器人是定制的Android板子,对系统做了裁剪,很多摄像头的方法可能就用不了

例如这样一个错误

1
AndroidRuntime: java.lang.RuntimeException: setParameters failed

当打开程序的时候,OpenCV会提示,没有找到可用摄像头或者摄像头被锁住(大概这个意思,我就不截图了),一种可能是设备真的没有接摄像头,也有可能是摄像头定制过,导致某些方法用不了,比如上面的错误就是我遇到的其中一个。

Android自定义摇杆

发表于 2016-09-01 | 分类于 开源 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


效果图

效果图

效果图

源码

KqwRockerDemo

喜欢就给个star,谢谢!

功能

  • 支持自适应大小
  • 支持2个方向、4个方向、8个方向的摇动监听
  • 支持摇动角度获取
  • 可选回调模式
  • 支持可摇动区域自定义
  • 支持摇杆自定义
  • 支持设置图片、色值、Shape图形

使用

1
2
3
4
5
6
7
8
<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 可选 摇杆半径

设置回调方式

1
setCallBackMode(CallBackMode mode)

参数

回调方式 描述
CALL_BACK_MODE_MOVE 有移动就立刻回调
CALL_BACK_MODE_STATE_CHANGE 状态有变化的时候回调

监听摇动角度

返回角度的取值范围:[0°,360°)

取值范围

1
setOnAngleChangeListener(OnAngleChangeListener listener)

监听摇动方向

1
setOnShakeListener(DirectionMode directionMode, OnShakeListener listener)

支持监听的方向

方向 图 描述
DIRECTION_2_HORIZONTAL 左右两个方向 横向 左右两个方向
DIRECTION_2_VERTICAL 上下两个方向 纵向 上下两个方向
DIRECTION_4_ROTATE_0 四个方向 四个方向
DIRECTION_4_ROTATE_45 四个方向 旋转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 中间

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
}
});
}

Android输出正弦波音频信号(左右声道对称)

发表于 2016-08-29 |

转载请说明出处!
作者:kqw攻城狮
出处:个人站 | CSDN


需求:左右声道分别输出不同的音频数据,波形要是一个正弦波,左右声道还要对称!
对硬件不是很了解,说是要通过音波避障。

效果图

效果图

之前已经介绍了如何在左右声道输出不同的音频数据。
那么这里主要介绍如何模拟出波形是正弦波的音频数据。

模拟正弦波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 模拟正弦波音频数据
* @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:采样率
1
2
3
4
5
6
// 单声道
private int mChannelConfig = AudioFormat.CHANNEL_OUT_MONO;
// 频率
private int mFrequency = 19000;
// 采样率
private int mSampleRateInHz = 44100;

播放音频的线程封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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;
}
}
}

播放

1
2
3
4
5
mLeftChannelThread = new ChannelThread(mChannelConfig, mSampleRateInHz, mDataLeft, bufferSize, true);
mRightChannelThread = new ChannelThread(mChannelConfig, mSampleRateInHz, mDataRight, bufferSize, false);
mLeftChannelThread.start();
mRightChannelThread.start();

停止

1
2
3
4
5
6
7
8
if (null != mLeftChannelThread) {
mLeftChannelThread.releaseAudioTrack();
mLeftChannelThread = null;
}
if (null != mRightChannelThread) {
mRightChannelThread.releaseAudioTrack();
mRightChannelThread = null;
}

不足

这里介绍的是在程序中模拟出一个波形满足正弦波的音频数据,还有一种方式,可以事先准备好一个这样的音频文件,直接播放就可以了。

在程序中模拟音频数据有一个缺点,就是不能保证两个线程完完全全的同步,即便是同时开启两个线程也有一先一后,在频率很高的时候,难免会有一点误差!像下面这样:

误差图

另外,这个波形和硬件有很大关系,越是低配设备,误差可能会越大,相同的趋势,但是波动的幅度会比较大(线很粗),可能和设备本身的噪音有关系。

123…6
kongqw

kongqw

Android Developer

54 日志
4 分类
25 标签
GitHub CSDN
Links
  • 干货集中营
  • 讯飞开放平台
  • 环信
  • 百度统计
  • DISQUS评论
  • BTC Wallet
  • OKCoin中国站
  • OKCoin国际站
  • BTC.com
© 2015 - 2018 kongqw
由 Hexo 强力驱动
主题 - NexT.Pisces