斜体文字被切掉了一块

TextView.setText的时候在右边加上一个空格。

或者string.xml中添加 来占位。

1
<string name="dp_page_ready">READY&#x200A;</string>

或者在TextView.setText的时候加上一个空格。

动态给LinearLayout添加子View

一列27个自定义view,如果要写到xml里就太麻烦了。
在Java代码中新建子View,设置LayoutParams,然后添加到LinearLayout里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private void initGrids() {
final int bigGridHeightPx = (int) dpToPx(10);
final int bigGrid2MarginVerticalPx = (int) dpToPx(3);
final int smallGridHeightPx = (int) dpToPx(6); // 这里有27个格点
final int smallGridMarginBotPx = (int) dpToPx(1);

LinearLayout linearLayoutLeft1 = findViewById(R.id.dp_page_g_field_left1);
LinearLayout linearLayoutLeft2 = findViewById(R.id.dp_page_g_field_left2);
mLeftGridList = new ArrayList<>();
GridsHorView g1 = new GridsHorView(this);
GridsHorView g2 = new GridsHorView(this);
GridsHorView g3 = new GridsHorView(this);

LinearLayout.LayoutParams p1 = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, bigGridHeightPx);
LinearLayout.LayoutParams p2 = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, bigGridHeightPx);
p2.setMargins(0, bigGrid2MarginVerticalPx, 0, bigGrid2MarginVerticalPx);
linearLayoutLeft1.addView(g1, p1);
linearLayoutLeft1.addView(g2, p2);
linearLayoutLeft1.addView(g3, p1);
mLeftGridList.addAll(Arrays.asList(g1, g2, g3));

for (int i = 0; i < 27; i++) {
GridsHorView smallGrid = new GridsHorView(this);
smallGrid.setAlphaValue((int) (255 * (1 - 0.02 * i)));
LinearLayout.LayoutParams sp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, smallGridHeightPx);
sp.setMargins(0, 0, 0, smallGridMarginBotPx);
mLeftGridList.add(smallGrid);
linearLayoutLeft2.addView(smallGrid, sp);
}

for (GridsHorView g : mLeftGridList) {
g.setOri(GridsHorView.Ori.RIGHT_TO_LEFT);
g.disableMode();
g.setCubeCount(1);
}

// 初始化右边(P2)的格子...
}

private float dpToPx(float dp) {
return dp * getResources().getDisplayMetrics().density;
}

获取当前WiFi的名字

需要定位权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static String getWiFiName(Context context) {
String wifiId = "WIFI_NAME_NOT_FOUND";
try {
WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wifiMgr.getConnectionInfo();
wifiId = info != null ? info.getSSID() : null;
} catch (Exception e) {
e.printStackTrace();
}
if (!TextUtils.isEmpty(wifiId) && wifiId.startsWith("\"")) {
wifiId = wifiId.substring(1); // 删去前面那个引号
}
return wifiId;
}

这个方法适用于判断WiFi名称的前缀。

检查权限的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void checkPermission(String[] permissions, final int reqCode) {
List<String> perList = new ArrayList<>();
for (String p : permissions) {
if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(this, p)) {
LL.e("没有权限: " + p);
perList.add(p);
}
}
if (!perList.isEmpty()) {
String[] per = new String[perList.size()];
for (int i = 0; i < per.length; i++) {
per[i] = perList.get(i);
}
ActivityCompat.requestPermissions(this, per, reqCode);
}
}

设置Click监听器

应用在Activity中,给一堆view设置同一个监听器

1
2
3
4
5
6
7
8
9
10
11
12
// 设置点击监听器
protected void setOnClickListeners(View.OnClickListener l, View... views) {
for (View v : views) {
v.setOnClickListener(l);
}
}

protected void setOnClickListeners(View.OnClickListener l, int... resIds) {
for (int r : resIds) {
findViewById(r).setOnClickListener(l);
}
}

设置字体

先把字体加载好。

1
2
3
4
5
6
7
8
9
10
11
protected void setTvPangMenAndItalic(int... tvResIds) {
for (int i : tvResIds) {
((TextView) findViewById(i)).setTypeface(AppControl.getPangMenTf(), Typeface.ITALIC);
}
}

protected void setTvPangMenAndItalic(TextView... tvs) {
for (TextView t : tvs) {
t.setTypeface(AppControl.getPangMenTf(), Typeface.ITALIC);
}
}

收起软键盘

1
2
3
4
5
6
7
8
void hideSoftKeyboard() {
try {
InputMethodManager inputMgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputMgr.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
} catch (Exception e) {
LL.e("rustApp", e);
}
}

判断网络是否有连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected boolean isNetworkAvailable(Context context) {
ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

if (connectivity == null) {
return false;
} else {
NetworkInfo[] info = connectivity.getAllNetworkInfo();
if (info != null) {
for (NetworkInfo anInfo : info) {
if (anInfo.getState() == NetworkInfo.State.CONNECTED) {
return true;
}
}
}
}
return false;
}

判断某个App是否安装

获取PackageManager通过包名来判断某个App是否安装。
但是有的手机在获取PackageManager的时候就能抛出异常。

1
2
3
4
5
6
7
8
9
10
protected boolean appInstalledOrNot(String uri) {
try {
PackageManager pm = getPackageManager();
pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
return true;
} catch (Exception e) {
LL.e("appInstalledOrNot: " + uri, e);
}
return false;
}

总而言之这并不是个很好的办法。

性能优化

一些性能优化的处理措施和思考。

给UI线程更多的CPU资源

数据库的操作放在子线程中进行。数据库线程也可能会和UI线程争抢CPU的时间片。
假设数据库要删除大量数据(比如1万条)。
那么我们可以尝试在数据库处理了某个数量(例如1千)的操作后,sleep一下,给UI线程让出CPU时间。
但现在一般都是多核手机,具体效果有待考量。

log记录工具

把log写到文件里。用RandomAccessFile与MappedByteBuffer写日志到文件中。任务处理放在子线程中,由HandlerThread来管理。

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
public class LL {

public static abstract class Level {
public static final String D = "D"; // 普通debug
public static final String W = "W"; // 警告
public static final String E = "E"; // 错误
}

private static String defTag = "App";

private static boolean showLogcat = true;
private static boolean writeFile = true;

// 注意申请SD卡读写权限
private static String logFileDir = Environment.getExternalStorageDirectory().getAbsolutePath() +
File.separator + "rust" + File.separator + "logs";

private static String fileName;

private static List<LogListener> listenerList = new ArrayList<>();

private static HandlerThread handlerThread;
private static Handler writerHandler;

private static final int LOG_FILE_GROW_SIZE = 1024 * 10; // log文件每次增长的大小
private static long gCurrentLogPos = 0; // log文件当前写到的位置 - 注意要单线程处理

/**
* 使用前必须调用此方法进行准备
*
* @param context 建议传入applicationContext
* @param fileDir 存放log文件的目录
*/
public static void prepare(Context context, @NonNull String fileDir, String logFilePrefix) {
gCurrentLogPos = 0;
if (TextUtils.isEmpty(fileDir)) {
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
logFileDir = Environment.getExternalStorageDirectory().getAbsolutePath() +
File.separator + "rust" + File.separator + "logs";
} else {
logFileDir = context.getFilesDir().getAbsolutePath() +
File.separator + "rust" + File.separator + "logs";
}
} else {
logFileDir = fileDir;
}
if (null == handlerThread) {
handlerThread = new HandlerThread("LL");
handlerThread.start();
}
writerHandler = new Handler(handlerThread.getLooper());
fileName = logFilePrefix + "_" + System.currentTimeMillis() + ".txt";
Log.d(defTag, "[prepare] file: " + fileName);
}

public static String getLogFileDir() {
return logFileDir;
}

public static String getFileName() {
return fileName;
}

// 退出
public static void quit() {
if (writerHandler != null) {
writerHandler.removeCallbacksAndMessages(null);
}
if (handlerThread != null) {
handlerThread.quit();
}
}

public static void setDefTag(String t) {
LL.defTag = t;
}

public static void setWriteFile(boolean w) {
LL.writeFile = w;
}

public static void d(String content) {
d(defTag, content, writeFile);
}

public static void d(String tag, String content) {
d(tag, content, writeFile);
}

public static void dn(String content) {
d(defTag, content, false);
}

// 不写到文件中
public static void dn(String tag, String content) {
d(tag, content, false);
}

public static void d(String tag, String content, boolean write) {
if (showLogcat) {
Log.d(tag, content);
}
tellLog(Level.D, tag, content);
if (write) {
if (writerHandler != null) {
writerHandler.post(new WriteRunnable(tag, content));
}
}
}

// log级别 WARN - w
public static void w(String content) {
w(defTag, content, writeFile);
}

public static void w(String tag, String content) {
w(tag, content, writeFile);
}

// 不写到文件中
public static void wn(String content) {
w(defTag, content, false);
}

// 不写到文件中
public static void wn(String tag, String content) {
w(tag, content, false);
}

public static void w(String tag, String content, boolean write) {
if (showLogcat) {
Log.w(tag, content);
}
tellLog(Level.W, tag, content);
if (write) {
if (writerHandler != null) {
writerHandler.post(new WriteRunnable(tag, content));
}
}
}

public static void e(String content) {
e(defTag, content);
}

public static void e(String tag, String content) {
e(tag, content, writeFile);
}

public static void e(String tag, Exception e) {
e(tag, e.getMessage(), writeFile);
}

// 只打log 不写文件
public static void en(String tag, String content) {
e(tag, content, false);
}

public static void e(String tag, String content, boolean write) {
if (showLogcat) {
Log.e(tag, content);
}
tellLog(Level.E, tag, content);
if (write) {
if (writerHandler != null) {
writerHandler.post(new WriteRunnable(tag, content));
}
}
}

private static void tellLog(String level, String tag, String content) {
if (null != listenerList) {
for (LogListener l : listenerList) {
l.onLog(level, tag, content);
}
}
}

public static void addListener(LogListener l) {
if (null == listenerList) {
listenerList = new ArrayList<>();
}
listenerList.add(l);
}

public static void removeListener(LogListener l) {
if (null != listenerList) {
listenerList.remove(l);
}
}

static class WriteRunnable implements Runnable {
String mmTag;
String mmContent;

WriteRunnable(String tag, String content) {
this.mmTag = tag;
this.mmContent = content;
}

@Override
public void run() {
SimpleDateFormat logTimeFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.CHINA);
String logContent = logTimeFormat.format(new Date()) + " [" + mmTag + "] " + mmContent + "\r\n";
try {
File dir = new File(logFileDir);
if (!dir.exists()) {
boolean mk = dir.mkdirs();
Log.d(defTag, "make dir " + mk);
}
File eFile = new File(logFileDir + File.separator + fileName);
byte[] strBytes = logContent.getBytes();
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(eFile, "rw");
MappedByteBuffer mappedByteBuffer;
final int inputLen = strBytes.length;
if (!eFile.exists()) {
boolean nf = eFile.createNewFile();
Log.d(defTag, "new log file " + nf);
mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE);
} else {
mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, inputLen);
}
if (mappedByteBuffer.remaining() < inputLen) {
mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE + inputLen);
// Log.d(defTag, "run: grow size ");
}
// Log.d(defTag, "run: gCurrentLogPos: " + gCurrentLogPos + ", pos: " + mappedByteBuffer.position() + ", remaining: " + mappedByteBuffer.remaining());
mappedByteBuffer.put(strBytes);
gCurrentLogPos += inputLen;
} catch (Exception e) {
Log.e(defTag, "WriteRunnable run: ", e);
if (!eFile.exists()) {
boolean nf = eFile.createNewFile();
Log.d(defTag, "new log file " + nf);
}
FileOutputStream os = new FileOutputStream(eFile, true);
os.write(logContent.getBytes());
os.flush();
os.close();
}
} catch (Exception e) {
e.printStackTrace();
Log.e(defTag, "写log文件出错: ", e);
}
}
}

}

监听器

1
2
3
public abstract class LogListener {
public abstract void onLog(String level, String tag, String content);
}

自定义View

一些自定义的view,比如一些折线图,条形图。

折线图 ColorAutoLineChart

单独一条折线,可自动缩放Y轴高度。使用FloatBuffer来存储数据。

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
public class ColorAutoLineChart extends View {

private static final String TAG = "AppColorAutoLineChart";

private float yMax = 1856f;
private float yMin = -1024f;

// 图表线条在view顶部留出的间距
float viewYStart = 2;
float axisTextSize = 7;

private int onShowPointsCount = 256; // 当前显示的数据个数
private int cacheMaxPoint = 9000; // 数据存储最大个数

float axisLineWid = 1f; // 坐标轴线条宽度
int dataLineWid = 2;

// 数据线颜色
private int dataColor = Color.WHITE;

private float xStep = 1.0f;
private float viewWidth;
private float viewHeight;
private float botLeftXOnView = 0; // 图表左下点在view中的x坐标
private float botLeftYOnView = 0;
private float originYToBottom = 20; // 图表原点距离view底部的距离

private FloatBuffer dataBuffer;

private Paint bgPaint;
private Paint linePaint;
private Paint wavePaint;
Path wavePath = new Path(); // 用来画渐变色
int waveTopColor = Color.parseColor("#f65212");

public ColorAutoLineChart(Context context) {
this(context, null);
}

public ColorAutoLineChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ColorAutoLineChart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

public void addData(int[] data) {
for (int i : data) {
addData(i);
}
}

public void addData(float data) {
dataBuffer.put(data);
if (dataBuffer.position() > (dataBuffer.capacity() * 2 / 3)) {
float[] bufferArr = dataBuffer.array();
System.arraycopy(bufferArr, dataBuffer.position() - cacheMaxPoint, bufferArr, 0, cacheMaxPoint);
dataBuffer.position(cacheMaxPoint);
// Log.d(TAG, "把当前数据移动到buffer起始位置 " + dataBuffer);
}
invalidate();
}

private void init(Context context) {
dataBuffer = FloatBuffer.allocate(3 * cacheMaxPoint); // 分配3倍的空间
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
wavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setStrokeWidth(axisLineWid);
bgPaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(dataLineWid);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(dataColor);

botLeftXOnView = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, context.getResources().getDisplayMetrics());
originYToBottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
viewYStart = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
axisLineWid = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
axisTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 8, context.getResources().getDisplayMetrics());
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = getWidth();
viewHeight = getHeight();
botLeftYOnView = viewHeight - originYToBottom;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.TRANSPARENT);
xStep = (viewWidth - botLeftXOnView) / (onShowPointsCount - 1);

int dataStartIndexInBuffer = 0; // 数据在buffer中的起始下标
if (dataBuffer.position() > onShowPointsCount) {
dataStartIndexInBuffer = dataBuffer.position() - onShowPointsCount;
}
float[] bufferArr = dataBuffer.array();
float maxData = bufferArr[0];
float minData = bufferArr[0];
for (int i = dataStartIndexInBuffer; i < dataBuffer.position(); i++) {
float cur = bufferArr[i];
if (cur < minData) {
minData = cur;
} else if (cur > maxData) {
maxData = cur;
}
}

drawWave(canvas, dataStartIndexInBuffer);
}

private void drawWave(Canvas canvas, int dataStartIndexInBuffer) {
wavePath.reset();
final float yDataRange = yMax - yMin;
final float yAxisRangeOnView = botLeftYOnView - viewYStart;
final float yDataStep = yAxisRangeOnView / yDataRange;

float[] dataArr = dataBuffer.array();
float maxData = dataArr[dataStartIndexInBuffer];

float waveStartX = botLeftXOnView;
float waveStartY = getYL(dataArr[dataStartIndexInBuffer], yDataStep);
wavePath.moveTo(waveStartX, waveStartY);

for (int i = dataStartIndexInBuffer; i < dataBuffer.position() - 1; i++) {
float curData = dataArr[i];
float nextData = dataArr[i + 1];
wavePath.lineTo(botLeftXOnView + (i - dataStartIndexInBuffer + 1) * xStep, getYL(nextData, yDataStep));
canvas.drawLine(botLeftXOnView + (i - dataStartIndexInBuffer) * xStep, getYL(curData, yDataStep),
botLeftXOnView + (i - dataStartIndexInBuffer + 1) * xStep, getYL(nextData, yDataStep),
linePaint);
maxData = Math.max(maxData, nextData);
}
wavePath.lineTo(viewWidth, viewHeight);
wavePath.lineTo(botLeftXOnView, viewHeight);
wavePath.lineTo(waveStartX, waveStartY);
wavePath.close();
wavePaint.setShader(new LinearGradient(0, getYL(maxData, yDataStep), 0, viewHeight, waveTopColor, Color.TRANSPARENT, Shader.TileMode.CLAMP));
canvas.drawPath(wavePath, wavePaint);
}

private float getYL(final float yData, float yDataStep) {
return botLeftYOnView - (yData - yMin) * yDataStep;
}

}

后台用户行为记录

最开始设计后台服务的时候,并没有考虑到太多的记录功能。仅仅记录了用户登录行为。
今后应该记录更详细的。例如获取用户信息,时间,客户端类型,userID等等。获取用户信息是否成功,可作为缓存登录的依据。

后台可以记录的用户行为,例如获取用户信息,用户查看飞行记录列表,用户查看飞行记录详情,用户点赞。

WebView设置

webview不显示图片的问题。LL后加载https的网页,默认会不加载http的资源。需要设置。

1
2
webSettings.setBlockNetworkImage(false);
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);

方法调用记录调用原因

调用某个方法的时候,比如中止某项功能。可以在log上记录一些原因,方便debug。

Glide

设定播放gif的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Glide.get(getApplicationContext()).setMemoryCategory(MemoryCategory.NORMAL);
Glide.with(this).asGif().listener(new RequestListener<GifDrawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<GifDrawable> target, boolean isFirstResource) {
return false;
}

@Override
public boolean onResourceReady(GifDrawable resource, Object model, Target<GifDrawable> target, DataSource dataSource, boolean isFirstResource) {
resource.setLoopCount(1);
return false;
}
}).load(R.drawable.app_start_up).into(imageView);
imageView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
MyAppControl.setPhoneStatusBarHeight(insets.getSystemWindowInsetTop());
return insets;
}
});