记录开发中遇到的 bug,不再让自己重复地被同样的 bug 折磨。
时间:2019年9月16日20:31:41
问题描述:在升级 butterknife 后报出这个错误。
问题分析:查看 https://github.com/JakeWharton/butterknife/blob/master/CHANGELOG.md#version-900-rc2-2018-11-19。
解决办法:
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
时间:2019年9月22日10:37:32
问题分析:
出错代码如下:
private float mapPx(Coordinate coordinate) {
switch (coordinate) {
case Coordinate.ZERO: // 这里编译报错:An enum switch case label must be the unqualified name of an enumeration constant.
return 0;
default:
return 0;
}
}
enum Coordinate {
ZERO(0),
HALF_WIDTH(1),
WIDTH(2),
HALF_HEIGHT(3),
HEIGHT(4);
private int coordinate;
Coordinate(int coordinate) {
this.coordinate = coordinate;
}
}
解决办法:
去掉 ZERO
前面的 Coordinate
,具体原因可以看 https://www.jianshu.com/p/380a503c7d37。
时间:2019年9月25日19:09:52
问题描述:最近上网比较困难,昨天连了同事的代理,可以上网了。但今天又挂了,好在公司提供了内网专线。但是,使用浏览器都可以访问外网的,Android Studio 却不可以 Sync,已经选择Settings->HttpProxy->NoProxy了。开始以为是网络不好的原因。
问题分析:旁边的同事,发现了虽然选择了 No Proxy,但还是会走昨天设置的代理。真是奇怪!!!最后发现在 C:\Users\Administrator.gradle\gradle.properties文件下,竟然还写着昨天的代理。
直接把方框里的内容注释掉。解决了这个问题。
时间:2019年10月23日14:42:30
解决办法:查看了自己的依赖,并不是因为重复依赖导致的。clean project 后正常编译。
时间:2019年10月23日14:44:45
解决办法:
在类库工程的 build.gradle 文件中添加:
android {
...
android.libraryVariants.all { variant ->
if (variant.buildType.name.equals('release')) {
variant.outputs.all {
def time = new Date().format("yyyyMMddHHmmss", TimeZone.getTimeZone("GMT+08"))
outputFileName = "yourlibname_v${defaultConfig.versionName}_${time}.aar"
}
}
}
}
时间:2019年10月23日20:22:49
问题描述:
应用增加了切换布局:网格布局和列表布局。对于这两种布局,分别设置有 ItemDecoration
:其中网格布局设置的是间距,列表布局设置的是分隔线。代码如下:
private void setupGridAdapter() {
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3));
recyclerView.removeItemDecoration(listItemDecoration);
recyclerView.addItemDecoration(gridItemDecoration);
recyclerView.setAdapter(adapterList.get(currentAdapterIndex));
}
private void setupListAdapter() {
clearRecylerViewItemDecorations();
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.removeItemDecoration(gridItemDecoration);
recyclerView.addItemDecoration(listItemDecoration);
recyclerView.setAdapter(adapterList.get(currentAdapterIndex));
}
实际上,在添加网格布局的 ItemDecoration
前,会移除列表布局的 ItemDecoration
;同样地,在添加列表布局的 ItemDecoration
前,也会移除网格布局的 ItemDecoration
。但是,实际测试发现,还是会出现分割线出现在网格布局里,列表布局的间距也出现了增大。这样的话,是非常影响 UI 效果的。
解决办法:
查看了 RecyclerView
的代码:
final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
public void removeItemDecoration(@NonNull ItemDecoration decor) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll or"
+ " layout");
}
mItemDecorations.remove(decor);
if (mItemDecorations.isEmpty()) {
setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
}
markItemDecorInsetsDirty();
requestLayout();
}
注意到,ItemDecoration
对象都是保存在 mItemDecorations
这个 List 里面。而每次调用 addItemDecoration
都是添加,而不是设置:
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or"
+ " layout");
}
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
这样就可能导致多次添加,其实打印 getItemDecorationCount()
的值也可以发现它的值会出现大于 1 的情况。
要是有一个 setItemDecoration()
方法该多好啊。这边就添加了一个这样的方法:
public void setItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
// 先取出所有的 ItemDecoration
List<RecyclerView.ItemDecoration> list = new ArrayList<>();
for (int i = 0; i < recyclerView.getItemDecorationCount(); i++) {
RecyclerView.ItemDecoration itemDecoration = recyclerView.getItemDecorationAt(i);
list.add(itemDecoration);
}
// 再移除所有的 ItemDecoration
for (RecyclerView.ItemDecoration itemDecoration : list) {
recyclerView.removeItemDecoration(itemDecoration);
}
// 最后添加新的 ItemDecoration
recyclerView.addItemDecoration(decor);
}
时间:2019年10月23日20:54:53
问题描述:
在友盟上捕获到这个错误,都是在 android O 以后出现的。
完整日志如下:
java.lang.IllegalStateException: Software rendering doesn't support hardware bitmaps
at android.graphics.BaseCanvas.throwIfHwBitmapInSwMode(BaseCanvas.java:532)
at android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:62)
at android.graphics.BaseCanvas.drawBitmap(BaseCanvas.java:120)
at android.graphics.Canvas.drawBitmap(Canvas.java:1434)
at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:529)
at android.widget.ImageView.onDraw(ImageView.java:1349)
at android.view.View.draw(View.java:19196)
at android.view.View.draw(View.java:19066)
at android.view.ViewGroup.drawChild(ViewGroup.java:4236)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4022)
at android.view.ViewOverlay$OverlayViewGroup.dispatchDraw(ViewOverlay.java:251)
at android.view.View.draw(View.java:19199)
at android.view.View.buildDrawingCacheImpl(View.java:18441)
at android.view.View.buildDrawingCache(View.java:18304)
at android.view.View.getDrawingCache(View.java:18210)
at android.view.View.getDrawingCache(View.java:18175)
at com.omnipotent.free.videodownloader.pro.utils.ViewUtils.captureView(ViewUtils.java:70)
at com.omnipotent.free.videodownloader.pro.ui.main.MainActivity.getCurrentTabsData(MainActivity.java:325)
at com.omnipotent.free.videodownloader.pro.ui.main.MainActivity.access$getCurrentTabsData(MainActivity.java:84)
at com.omnipotent.free.videodownloader.pro.ui.main.MainActivity$initView$5.onClick(MainActivity.java:252)
at android.view.View.performClick(View.java:6294)
at android.view.View$PerformClick.run(View.java:24774)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6518)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
定位到一个把 view
转成 Bitmap
的方法:
fun captureView(view: View): Bitmap {
val tBitmap = Bitmap.createBitmap(
view.width, view.height, Bitmap.Config.RGB_565
)
val canvas = Canvas(tBitmap)
view.draw(canvas)
canvas.setBitmap(null)
return tBitmap
}
问题分析:
查询网上资料,Glide 文档上的硬件位图 讲的非常详细。其中提到了哪些情况不能使用硬件位图? 有一个情况是:
在代码中触发截屏操作,它会尝试使用 Canvas 来绘制视图层级。
作为一个替代方案,在 Android O 以上版本你可以使用 PixelCopy.
很明显,Glide 文档建议使用 PixelCopy
这个类,来解决在代码中触发截屏操作导致的异常。
解决办法:
所以这边采用的方案是在 android O 以后使用 PixelCopy
来获取 Bitmap
,在 android O 以下还是使用原方案。代码如下:
fun captureView(view: View, window: Window, bitmapCallback: (Bitmap)->Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LogUtils.dTag(TAG, "captureView version O 以上")
// 高于 O 的,使用 PixelCopy
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val location = IntArray(2)
view.getLocationInWindow(location)
PixelCopy.request(window,
Rect(location[0], location[1], location[0] + view.width, location[1] + view.height),
bitmap,
{
if (it == PixelCopy.SUCCESS) {
LogUtils.dTag(TAG, "captureView 获取到 bitmap")
bitmapCallback.invoke(bitmap)
} else {
LogUtils.dTag(TAG, "captureView 未获取到 bitmap, copyResult = $it")
}
},
Handler(Looper.getMainLooper()) )
} else {
LogUtils.dTag(TAG, "captureView version O 以下")
val tBitmap = Bitmap.createBitmap(
view.width, view.height, Bitmap.Config.RGB_565
)
val canvas = Canvas(tBitmap)
view.draw(canvas)
canvas.setBitmap(null)
bitmapCallback.invoke(tBitmap)
}
}
需要特别说明的是,这里通过回调获取 Bitmap
,原因是 PixelCopy
需要通过回调返回 Bitmap
。从目前的友盟错误上已经看不到这个异常了,说明改动是有效的。
同时可以关注一下,我在 Stack Overflow 上提的这个问题:java.lang.IllegalStateException: Software rendering doesn’t support hardware bitmaps 。
时间:2019年10月31日16:18:39
问题描述:在编译项目时报出这个异常。
解决办法:发现自己的依赖版本不一致:
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor "com.github.bumptech.glide:compiler:4.8.0"
把 4.8.0 改成 4.9.0 后运行正常。
时间:2019年11月30日13:44:21
问题描述:在集成了一个功能模块的代码后,发现在 Honor Play 手机上运行后,图标变为了默认的机器人。
解决办法:因为在没有集成这个功能模块之前,桌面的图标是正常的,所以就去查看一下这个功能模块。查看后,发现在这个功能模块里存在默认的机器人图标,名字是 ic_launcher。而这个模块并不需要 ic_launcer 的图片资源。果断删除它们,重新运行后桌面图标显示正常。
时间:2019年11月30日13:50:48
问题描述:
我们会遇到这样的情况,开启异步线程加载数据,同时显示 ProgressBar;在获取到数据后,就把之前显示的 ProgressBar 隐藏掉。但问题是,如果间隔时间较短,就会看到 ProgressBar 在屏幕上一闪而过。这样的用户体验是较差的。
解决办法:
之前自己的解决办法是在异步线程里故意去增加一点延时,比如 300 ms。但是这多么暴力啊,有略显无脑。那么有没有更好的办法呢?有的有的。那就是 ContentLoadingProgressBar
类,是官方库里面的。这个类就是为了解决这样的问题而生的。这里把源码放出来,方便及时查看:
package androidx.core.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be
* dismissed before showing. Once visible, the progress bar will be visible for
* a minimum amount of time to avoid "flashes" in the UI when an event could take
* a largely variable time to complete (from none, to a user perceivable amount)
*/
public class ContentLoadingProgressBar extends ProgressBar {
private static final int MIN_SHOW_TIME = 500; // ms
private static final int MIN_DELAY = 500; // ms
long mStartTime = -1;
// 发送隐藏任务的标记
boolean mPostedHide = false;
// 发送显示任务的标记
boolean mPostedShow = false;
// 被清除的标记
boolean mDismissed = false;
private final Runnable mDelayedHide = new Runnable() {
@Override
public void run() {
mPostedHide = false;
mStartTime = -1;
setVisibility(View.GONE);
}
};
private final Runnable mDelayedShow = new Runnable() {
@Override
public void run() {
mPostedShow = false;
if (!mDismissed) {
mStartTime = System.currentTimeMillis();
setVisibility(View.VISIBLE);
}
}
};
public ContentLoadingProgressBar(@NonNull Context context) {
this(context, null);
}
public ContentLoadingProgressBar(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
removeCallbacks();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks();
}
private void removeCallbacks() {
removeCallbacks(mDelayedHide);
removeCallbacks(mDelayedShow);
}
/**
* Hide the progress view if it is visible. The progress view will not be
* hidden until it has been shown for at least a minimum show time. If the
* progress view was not yet visible, cancels showing the progress view.
* 如果进度控件是可见的,就隐藏它。进度控件不会被隐藏知道它已经至少显示了一段最小
* 的显示时间。如果进度控件不可见,就删除显示进度控件。
*/
public synchronized void hide() {
mDismissed = true;
removeCallbacks(mDelayedShow);
mPostedShow = false;
long diff = System.currentTimeMillis() - mStartTime;
if (diff >= MIN_SHOW_TIME || mStartTime == -1) {
// The progress spinner has been shown long enough
// OR was not shown yet. If it wasn't shown yet,
// it will just never be shown.
setVisibility(View.GONE);
} else {
// The progress spinner is shown, but not long enough,
// so put a delayed message in to hide it when its been
// shown long enough.
if (!mPostedHide) {
postDelayed(mDelayedHide, MIN_SHOW_TIME - diff);
mPostedHide = true;
}
}
}
/**
* Show the progress view after waiting for a minimum delay. If
* during that time, hide() is called, the view is never made visible.
* 在等待一个最小的延时时间后才显示进度控件。如果在那段最小的延时时间内,
* hide() 方法被调用了,那么这个进度控件就不会变为可见了。
*/
public synchronized void show() {
// Reset the start time. 重置开始时间
mStartTime = -1;
mDismissed = false;
removeCallbacks(mDelayedHide);
mPostedHide = false;
if (!mPostedShow) {
postDelayed(mDelayedShow, MIN_DELAY);
mPostedShow = true;
}
}
}
代码出错了,关键是要仔细查看日志。能够仔细地查看日志,就离解决问题很近了。