更新提示:
<更新19,关于新硬件支撑刷新率90Hz,120Hz fps>
<更新20,沉浸式和fitSystemWindows 配合解决方案>
<更新21,加速Android Studio 编译的插件>
1. IllegalStateException: Can not perform this action after onSaveInstanceState
Fatal Exception: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:1844)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1884)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:329)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:294)
at com.amfun.home.fragment.HomeFragment.initView(HomeFragment.java:124)
at com.amfun.common.mvvm.BaseFragment.onCreateView(BaseFragment.java:79)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2963)
可能的原因之一:
Fragment 在显示或者隐藏,移除是出现Can not perform this action after onSaveInstanceState #解决办法:onSaveInstanceState方法是在该Activity即将被销毁前调用,来保存Activity数据的,如果在保存玩状态后 再给它添加Fragment就会出错。解决办法就是把commit()方法替换成 commitAllowingStateLoss()
可能的原因之二:Activity被系统回收之后,重建时恢复缓存的Fragment的原因 #解决办法之一:在Activity 回收时 onSaveInstanceState 中不缓存Fragment ,在OnCreate 中移除缓存相应Fragment数据,代码如下
private static final String BUNDLE_FRAGMENTS_KEY = "android:support:fragments";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null && this.clearFragmentsTag()) {
//重建时清除 fragment的状态
savedInstanceState.remove(BUNDLE_FRAGMENTS_KEY);
}
super.onCreate(savedInstanceState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (outState != null && this.clearFragmentsTag()) {
//销毁时不保存fragment的状态
outState.remove(BUNDLE_FRAGMENTS_KEY);
}
}
protected boolean clearFragmentsTag() {
return true;
}
2. File not found ,或者读取相册缓慢
Android 10 的权限兼容问题
描述更清晰,在做相册视频选择的时候,列表获取失败anr,获取置顶视频文件找不到的情况
检查其他应用,微博,小红书,抖音都正常,用的第三方库也是正常。那么这个时候考虑系统权限等兼容
解决,在Application中加入android:requestLegacyExternalStorage="true"
类似我们要访问指定安全域名android:networkSecurityConfig
<application
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
3. 关于启动Activity和退出Activity的动画添加,我先换叫Controller,为了和iOS保持一致
你可能也遇到过,不懂得系统的设计师,非要和iOS保持一致,不得已自己覆盖系统启动Activity的动画。
这是一个简单问题,但是又比较烦。
我这里就简单的记录一下,通用主题的方案,不再用代码一个一个侵入代码Base类。如果设计师花样频出,你就没办法,用base类封装吧。
主题添加,动画添加,网上的你还有调试,根据这个不需要太多调试,尤其是csdn没法看
「slide_in_right_anim.xml」从右侧启动进入 《-
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromXDelta="50%p"
android:toXDelta="0" />
<alpha
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="1.0" />
</set>
「slide_out_left_anim.xml」从右侧滑动退出。 -》
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromXDelta="0"
android:toXDelta="100%p" />
<alpha
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="1.0" />
</set>
满足那个经验不足的设计需求,非资质设计师。
ps:警告特殊情况会因为这个影响你的性能。
4. ViewPager 需求不要滑动,但是可能会闪动,解决问题
通常禁止滑动的需求,如果有显示隐藏动态Gone掉也会有问题,所以会禁止滑动。
public class CustomViewPager extends ViewPager {
private boolean isCanScroll = true;
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomViewPager(Context context) {
super(context);
}
public void setCanScroll(boolean isCanScroll) {
this.isCanScroll = isCanScroll;
}
@Override
public boolean onTouchEvent(MotionEvent arg0) {
if (isCanScroll) {
return super.onTouchEvent(arg0);
} else {
return false;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent arg0) {
if (isCanScroll) {
return super.onInterceptTouchEvent(arg0);
} else {
return false;
}
}
@Override
public void setCurrentItem(int item, boolean smoothScroll) {
super.setCurrentItem(item, smoothScroll);
}
@Override
public void setCurrentItem(int item) {
super.setCurrentItem(item, false);
}
}
5. 关于息屏,常用简单解决。
场景是在 Activity中,进行播放等活动,超出系统设置最大息屏时间,保持亮屏播放
onCreate 函数添加
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
6. Recyclerview 嵌套时, item点击事件处理,子Recyclerview不响应父Recyclerview的点击 ----CymChad 的Kotlin版本的BaseQuickAdapter
参考:https://github.com/cymchad/baserecyclerviewadapterhelper/issues/2516 这个文章解决一些你可能有的问题。
明确一个点击问题,就是需要现在外层adapter,把item的点击需要的事件加入
init {
addChildClickViewIds(R.id.iv_nice_head)
addChildClickViewIds(R.id.player_container)
addChildClickViewIds(R.id.prepare_view)
}
然后在adapter调用
mHomeListAdapter.setOnItemChildClickListener((adapter, view12, position) -> {
if (view12.getId() == R.id.iv_nice_head) {
...
}
...
}
7. chrome 无法直接播放m3u8视频。
安装插件
native-hls-playback
去chrome商店搜索
11.12.59.png
8. 代码注释乱,structure里面查找代码困难
参考了SmartRefreshlayout的注视,了解了这个奇技淫巧
包装了代码块后,直接不用看代码,就能知道折叠部分的功能
//<editor-fold desc="功能属性">
.......code.....
//</editor-fold>
9. E/RecyclerView: No layout manager attached; skipping layout
有时候我们写代码或者提交冲突,导致疏忽,这个只能解决一般性问题。
//必须先设置LayoutManager
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
10.Firebase uploadCrashlyticsMappingFileRelease 失败 解决方法
会出现莫名其妙的打包失败现象,打包失败,关闭自动上传
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
//zipAlignEnabled 优化
zipAlignEnabled true
// 设置是否要自动上传
firebaseCrashlytics {
mappingFileUploadEnabled false
}
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
minifyEnabled false
versionNameSuffix "_debug"
}
}
11. Facebook 推广应用,在Android上面的测试问题
这个问题其实,以前遇到过,但是,最近又遇到这种问题验证,慌了一下。
》1 广告推官人员,给你一个测试链接,就是一个短链接,内容是自己定义的协议。
需要在浏览器打开,然后跳转到Facebook的app内部。这个时候问题出现了,我看不到fb 的官网广告测试怎么办。
到浏览器打开,这个链接,登陆,然后点击,显示广告,到fb 的app看,这个时候有广告。因为fb在手机上的缓存做的太严重了,导致会出现刷新一次广告,下次就没有了的问题。借助浏览器,容易清理缓存的机制,可以作为广告工具辅助测试。
》2 发现点击广告----〉跳转到GooglePlay下载,安装后ApplinkData回掉是空的。
那么问题解决:
需要按照步骤来,清理浏览器缓存,杀死fb的应用进程。到pc浏览器,打开广告推广发给你的链接测试,pc浏览器点击显示广告,到fb的app查看广告,---->点下载-----》重点来了,这个时候是在fb的app内部弹出GooglePlay,《----这样才会被Deeplink标记,下载打开,才会出现ApplindData回调数据。解析数据完成业务。
说了这么多,这个ApplinkData是干什么的,就是花钱投广告,通过广告下载GooglePlay的应用,应用内你知道是Deeplink方式过来的,后台统计数据,用来分桶测试,还有测试应用的市场效果。
12. 手上没有足够分辨率手机测试怎么办,可以借助adb工具实现
通常我们测试分辨率,需要用各种手机,其实没有那么麻烦
进入adb
默认的分辨率查看
wm size
$ wm size 1920x1280 (小写的x)修改分辨率
查看dpi
wm density
修改dpi
wm density 240 修改dpi
重置
$ wm size reset
$ wm density reset
12. DialogFragment 或者一些弹窗中,需要滑动关闭的时候
算不上问题和bug,但是是个小的细节。尤其是产品要求和iOS的效果一致
view.setOnTouchListener((v, event) -> {
float y = event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
offsetY = y - lastY;
if (offsetY > 0) {
ViewHelper.setTranslationY(view, offsetY);
}
break;
case MotionEvent.ACTION_UP:
if (offsetY > 0) {
if (offsetY < view.getHeight() / 3) {
ViewHelper.setTranslationY(view, 0f);
} else {
dismiss();
}
}
break;
default:
break;
}
return true;
});
13. network 获取网络信息,Android个种改系统源码,不兼容
问题:
错误的获取
ConnectivityManager connectionManager = (ConnectivityManager) RouteForMeApplication.getInstance()
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectionManager == null) return false;
boolean networkAvailable = connectionManager.getActiveNetworkInfo() != null
&& connectionManager.getActiveNetworkInfo().isConnected();
return networkAvailable;
Kotlin 修正,更改调用api
class NetworkUtilsKt {
private var isAvailable = false
private val context = BaseApplication.getInstance().applicationContext
private var macAddress: String = ""
private var ipAddresses: String = ""
/**
* 判断是否有网络
*
* @return
*/
fun isNetworkConnected(): Boolean {
return isAvailable
}
fun getMacAddress(): String{
return macAddress
}
fun getIpAddress(): String{
return ipAddresses
}
/**
* 将ip的整数形式转换成ip形式
*
* @param ipInt
* @return
*/
private fun int2ip(ipInt: Int): String{
val sb = StringBuilder()
sb.append(ipInt and 0xFF).append(".")
sb.append(ipInt shr 8 and 0xFF).append(".")
sb.append(ipInt shr 16 and 0xFF).append(".")
sb.append(ipInt shr 24 and 0xFF)
return sb.toString()
}
private fun convertToMac(mac: ByteArray?): String {
var str: String = ""
try {
val sb = java.lang.StringBuilder()
for (i in mac?.indices!!) {
val b = mac[i]
var value = 0
when {
b in 0..16 -> {
value = b.toInt()
sb.append("0" + Integer.toHexString(value))
}
b > 16 -> {
value = b.toInt()
sb.append(Integer.toHexString(value))
}
else -> {
value = 256 + b
sb.append(Integer.toHexString(value))
}
}
if (i != mac.size - 1) {
sb.append(":")
}
}
str = sb.toString()
if (str.startsWith("0:")) {
str= "0$str"
}
} catch (e: Exception) {
KLog.e(Constant.DebugKeyInfo.filter_exception, e.toString())
}
return str
}
init {
val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val request = NetworkRequest.Builder().build()
connectivityManager.registerNetworkCallback(request,object : ConnectivityManager.NetworkCallback(){
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
KLog.i(Constant.DebugKeyInfo.filter_exception, "net status change! 网络连接改变")
parseNetworkCapabilities(networkCapabilities)
}
override fun onLost(network: Network) {}
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {}
override fun onUnavailable() {
isAvailable = false
}
override fun onLosing(network: Network, maxMsToLive: Int) {}
override fun onAvailable(network: Network) {
connectivityManager.getNetworkCapabilities(network)?.also {
parseNetworkCapabilities(it)
}
isAvailable = true
}
})
}
@SuppressLint("HardwareIds")
private fun parseNetworkCapabilities(networkCapabilities: NetworkCapabilities){
try {
var networkType = "eth0"
// 表明此网络连接成功验证
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
// 使用WI-FI
val wifi = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as WifiManager
ipAddresses = int2ip(wifi.connectionInfo.ipAddress)
networkType = "wlan0"
} else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|| networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)){
// 使用蜂窝网络
// 使用有线网络
networkType = "eth0"
}
}
val en: Enumeration<NetworkInterface> =
NetworkInterface.getNetworkInterfaces()
while (en.hasMoreElements()) {
val networkInterface: NetworkInterface = en.nextElement()
if (networkInterface.displayName.toLowerCase(Locale.getDefault()) == networkType){
val enumeration: Enumeration<InetAddress> = networkInterface.inetAddresses
while (enumeration.hasMoreElements()) {
val netAddress: InetAddress = enumeration.nextElement()
if (!netAddress.isLoopbackAddress && netAddress is Inet4Address) {
ipAddresses = netAddress.hostAddress.toString()
macAddress = convertToMac(networkInterface.hardwareAddress)
break
}
}
}
}
} catch (ex: SocketException) {
KLog.e(Constant.DebugKeyInfo.filter_exception, ex.toString())
}
}
companion object{
val proxy = NetworkUtilsKt()
}
}
14. tryGetViewHolderForPositionByDeadline
bug 未能解决
15. Cannot call this method while RecyclerView is computing a layout or scrolling
bug 未能解决
16. 解决获取View位置的问题
- 在FragmentActivity中获取
onWindowFocusChanged()
从字面上就可以知道,这个方法是在窗口焦点发生变化时被调用。
启动一个activity(要注意如果该手机或模拟器处于锁屏状态时,这时只走onCreate()、onStart()、onResume(),不会再走onWindowFocusChanged()
按下home键,再回到activity
从当前activty回到上一个activity
结合activity/fragment生命周期,能得到很多
所以用的频率并不多。
- Activity 内部的Fragment想要拿到View的位置在onCreate等拿到的通常都是0
所以需要在ViewTreeObserver 中获取
mBinding.bisPlaceholder.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mBinding.bisPlaceholder.getLocationOnScreen(location);
KLog.e("zcw_", "location:" + location[0] + "," + location[1]);
}
});
- 我们有几种获取的方式
第一种:
int location[] = new int[2];
getLocationOnScreen(@Size(2) int[] outLocation)
或者直接
getLocationInWindow(@Size(2) int[] outLocation)
用过C++的,应该都熟悉这种套路
第二种:
Rect rect = new Rect();
boolean getGlobalVisibleRect(Rect r)
还有
boolean getLocalVisibleRect(Rect r)
如上,都是输出到定义的对象中
17 E/DecorView: mWindow.mActivityCurrentConfig is null
这可能是设备问题,可以忽略它。 我已经搜索了几个小时,此时只要错误不会使设备崩溃,您就可以忽略它
18 RecyclerView.setHasFixedSize(true)
setHasFixedSize === true 导致notifyItem系列方法不起作用,
可以用notifyDataSet 刷新。具体api看。提一句
19 关于新硬件支撑刷新率90Hz,120Hz fps
最近遇到同事吐槽,太慢,看到其他家应用都是120Hz fps。所以网上找到了解决方案。
我略微改动
Java 版本
/*
M 是 6.0,6.0修改了新的api,并且就已经支持修改window的刷新率了。
但是6.0那会儿,也没什么手机支持高刷新率吧,所以也没什么人注意它。
我更倾向于直接判断 O,也就是 Android 8.0,我觉得这个时候支持高刷新率的手机已经开始了。
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 获取系统window支持的模式
Display.Mode[] modes = getWindow().getWindowManager().getDefaultDisplay().getSupportedModes();
Display.Mode fpsMode = null;
for (Display.Mode mode : modes) {
float fps = mode.getRefreshRate();
KLog.e("amfun_fps",fps);
if(fps >= 90){
fpsMode = mode;
break;
}
}
if(fpsMode != null) {
WindowManager.LayoutParams layoutParams= getWindow().getAttributes();
if(layoutParams != null) {
layoutParams.preferredDisplayModeId = fpsMode.getModeId();
}
getWindow().setAttributes(layoutParams);
}
}
Kotlin 版本
/*
M 是 6.0,6.0修改了新的api,并且就已经支持修改window的刷新率了。
但是6.0那会儿,也没什么手机支持高刷新率吧,所以也没什么人注意它。
我更倾向于直接判断 O,也就是 Android 8.0,我觉得这个时候支持高刷新率的手机已经开始了。
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 获取系统window支持的模式
val modes = window.windowManager.defaultDisplay.supportedModes
// 对获取的模式,基于刷新率的大小进行排序,从小到大排序
modes.sortBy {
it.refreshRate
}
window.let {
val lp = it.attributes
// 取出最大的那一个刷新率,直接设置给window
lp.preferredDisplayModeId = modes.last().modeId
it.attributes = lp
}
}
在 Activity的onCreate之前调用
关于适配Android 11 的setFrame,我没有找到合适的设置位置和具体如何使用
20. fitsSystemWindows和设置沉浸式,对当前布局的insets做一些处理
问题原因:最近在看Compose开源项目,发现每个项目中都在开头用了fitsSystemWindows 等等。所以想起来以前一些沉浸式问题,总结下。
下面两个的区别
1. android:fitsSystemWindows + FragmentLayout
2. android:fitsSystemWindows + CoordinaryLayout
CoordinatorLayout 做了这些事:
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
对当前布局的insets做一些处理,并且调用了上面一行代码
fitsSystemWindows 的配合
FragmentLayout 方案 , 因为这里没有像ConstrantLayout 内部的处理inset,所以我们代码要特殊处理下
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
android:background="#ff66ff">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/photo"
android:fitsSystemWindows="true"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"/>
</FrameLayout>
用了心的api你会发现,这里很多代码过时了
window.statusBarColor = Color.TRANSPARENT
// FrameLayout + android:fitsSystemWindows
val frameLayout = findViewById<FrameLayout>(R.id.root_layout)
frameLayout.systemUiVisibility = (SYSTEM_UI_FLAG_LAYOUT_STABLE
or SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
//setOnApplyWindowInsetsListener ()函数去监听WindowInsets发生变化
val button = findViewById<Button>(R.id.button)
ViewCompat.setOnApplyWindowInsetsListener(button) { view, insets ->
val params = view.layoutParams as FrameLayout.LayoutParams
params.topMargin = insets.systemWindowInsetTop
insets
}
}
ConstraintLayout 的方式似乎代码少些
window.statusBarColor = Color.TRANSPARENT
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff66ff"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/photo" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<Button
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</androidx.constraintlayout.widget.ConstraintLayout>
完全使用主题解决也许是一个方案,但是复杂的项目问题,并不是唯一好的解决方式
<style name="AppTheme_light_immersive" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowNoTitle">true</item>
</style>
但是在6.0 可能需要代码适配
Window window = activity.getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
21. 加速编译的AS 插件,不一定完全有用
22. 老项目升级gradle,涉及到编译不通过问题
排查:
- 老规矩,project的gradle 插件升级. gradle-wrapper.properties 的 版本 distributionUrl 升级
- 编译排查,各种第三方库,是否有脚本语法错误
- 兼容性,通常情况,是第三方工具库导致,无非数据库,图片库,语音库等。
我这里提一下greendao 必须3.3.0之上。
参考存储文档: https://developer.android.google.cn/training/data-storage?hl=zh-cn
【持续更新,欢迎关注】
网友评论