斜体文字被切掉了一块
TextView.setText的时候在右边加上一个空格。
或者string.xml中添加 
来占位。
<string name="dp_page_ready">READY </string>
或者在TextView.setText的时候加上一个空格。
动态给LinearLayout添加子View
一列27个自定义view,如果要写到xml里就太麻烦了。 在Java代码中新建子View,设置LayoutParams,然后添加到LinearLayout里。
代码语言:javascript复制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的名字
需要定位权限。
代码语言:javascript复制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名称的前缀。
检查权限的方法
代码语言:javascript复制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设置同一个监听器
代码语言:javascript复制// 设置点击监听器
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);
}
}
设置字体
先把字体加载好。
代码语言:javascript复制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);
}
}
收起软键盘
代码语言:javascript复制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);
}
}
判断网络是否有连接
代码语言:javascript复制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的时候就能抛出异常。
代码语言:javascript复制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来管理。
代码语言:javascript复制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 "rn";
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);
}
}
}
}
监听器
代码语言:javascript复制public abstract class LogListener {
public abstract void onLog(String level, String tag, String content);
}
自定义View
一些自定义的view,比如一些折线图,条形图。
折线图 ColorAutoLineChart
单独一条折线,可自动缩放Y轴高度。使用FloatBuffer来存储数据。
代码语言:javascript复制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的资源。需要设置。
代码语言:javascript复制webSettings.setBlockNetworkImage(false);
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
方法调用记录调用原因
调用某个方法的时候,比如中止某项功能。可以在log上记录一些原因,方便debug。
Glide
设定播放gif的次数
代码语言:javascript复制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;
}
});