Problems专题之Gradle

Problems专题之Gradle

0x01 More than one file was found with OS independent path ‘META-INF/webview_release.kotlin_module’

这是因为第三方库中存在很多重名的META-INF文件,在打包的时候去除即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
android {
// ...
packagingOptions {
exclude 'META-INF/webview_release.kotlin_module'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'META-INF/gradle/incremental.annotation.processors'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/ASL2.0'
// ...
}
}

0x02 Certificate for <x.x.x> doesn’t match any of the subject alternative names

在执行gradlew命令打包时遇到这个错误,肯定是https证书有问题。

解决方案:如果支持http的话就使用http

1
2
3
4
5
6
7
8
9
> Could not resolve com.bytedanceapi:ttsdk-ttmp:1.36.2.25.pcdn.
Required by:
project :player > com.bytedanceapi:ttsdk-player_premium:1.36.2.25.pcdn > com.bytedanceapi:ttsdk-ttplayer:1.36.2.25.pcdn
> Could not resolve com.bytedanceapi:ttsdk-ttmp:1.36.2.25.pcdn.
> Could not get resource 'https://artifact.bytedance.com/repository/Volcengine/com/bytedanceapi/ttsdk-ttmp/1.36.2.25.pcdn/ttsdk-ttmp-1.36.2.25.pcdn.pom'.
> Could not GET 'https://artifact.bytedance.com/repository/Volcengine/com/bytedanceapi/ttsdk-ttmp/1.36.2.25.pcdn/ttsdk-ttmp-1.36.2.25.pcdn.pom'.
> Certificate for <artifact.bytedance.com> doesn't match any of the subject alternative names: [*.alicdn.com, *.cmos.greencompute.org, cmos.greencompute.org, m.intl.taob
ao.com, *.mobgslb.tbcache.com, alikunlun.com, *.alikunlun.com, s.tbcdn.cn, *.django.t.taobao.com, alicdn.com]

把项目根目录的build.gradle文件中所有的https://artifact.bytedance.com/替换为http://artifact.bytedance.com/即可

Ox03 module java.base does not “opens java.io” to unnamed module

升级到 Java 16 以上,AndroidStudio编译遇到错误。

Unable to make field private final java.lang.String java.io.File.path accessible: module java.base does not “opens java.io” to unnamed module @fb04536

解决方案一:

  1. gradle-wrapper 属性中的 gradle 版本更改为 7.1.1(6.x 不支持 java 16)
  2. gradle.properties 中添加以下行
1
2
3
4
5
6
org.gradle.jvmargs=-Xmx1536m \
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens=java.base/java.io=ALL-UNNAMED \
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED

解决方案二:

升级build-gradlew版本。 将项目根目录的 build.gradle文件中
classpath 'com.android.tools.build:gradle:4.2.2'
升级为
classpath 'com.android.tools.build:gradle:7.2.1'

Problems专题:Provider

Problems专题:Provider

0x01 FileProvider:Failed to find configured root that contains /…/**.jpeg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/DCIM/Camera/**.jpeg
at androidx.core.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:800)
at androidx.core.content.FileProvider.getUriForFile(FileProvider.java:442)
at com.luck.picture.lib.tools.PictureFileUtils.parUri(PictureFileUtils.java:533)
at com.luck.picture.lib.PictureBaseActivity.startOpenCameraImage(PictureBaseActivity.java:705)
at com.luck.picture.lib.PictureSelectorActivity.startCamera(PictureSelectorActivity.java:926)
at com.luck.picture.lib.PictureSelectorActivity.onTakePhoto(PictureSelectorActivity.java:1473)
at com.luck.picture.lib.adapter.PictureImageGridAdapter.lambda$onBindViewHolder$0$PictureImageGridAdapter(PictureImageGridAdapter.java:155)
at com.luck.picture.lib.adapter.-$$Lambda$PictureImageGridAdapter$0EODmJcP4VP0lqmkEhQ1dzLbHi8.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:6608)
at android.view.View.performClickInternal(View.java:6585)
at android.view.View.access$3100(View.java:785)
at android.view.View$PerformClick.run(View.java:25921)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6810)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
Back traces end.

问题分析

  1. FileProvider 路径配置文件被互相覆盖
  2. FileProvider 路径配置文件id重复,导致覆盖

解决方案

Problems专题:编译环境

Problems专题:编译环境

0x01 java.lang.AssertionError: Could not delete caches dir

CreateProcess error=206, El nombre del archivo o la extensión es demasiado largo

Caused by: java.lang.AssertionError: Could not delete caches dir YourProjectPath\build\kotlin\compileDebugTestingKotlin

临时解决

打开任务管理器,结束 java.exe 或者 OpenJDK Platform Binary

降级 Android Studio

Notice: This happens with the newer AndroidStudio 4.2.x.

Google hasn’t provide us a fix, so you’ll need to downgrade to an older version which works for you. 4.1.3 seems to be working fine.

参考链接:https://stackoverflow.com/questions/65832868/caused-by-java-lang-assertionerror-could-not-delete-caches-dir-yourproject-bui

0x02 Please close other application using ADB:Monitor, DDMS, Eclip

Warning:debug info can be unavailable. Please close other application using ADB:Monitor, DDMS, Eclipse.

方案一:

1
$ adb usb

方案二:

打开任务管理器,结束adb.exe进程。

方案三:

重启 adb 服务

1
2
$ adb kill-server
$ adb start-server

0x03 能安装apk却无法查看log[真机偶现]

能安装apk却无法在Logcat查看log,即使重启Android studio,重启adb服务都无法解决。最后通过重启手机搞定

0x04 platform-tools/api/api-versions.xml java.io.IOException: Stream closed

在 android studio 更新到 v2020.3.1 后遇到

1
cannot load api descriptions from ../Android/android-sdk/platform-tools/api/api-versions.xml java.io.IOException: Stream closed

问题的原因与类SdkUtils (请参阅the source file)相关。SdkUtils类具有对文件platform-tools/api/api-versions.xml的硬引用,但是在最新的平台工具(31.0.3)中,该文件不再存在。

从platforms/android-31/data/api-versions.xml复制文件到platform-tools/api/api-versions.xml。

如果是CI编译,可以尝试以下脚本:

1
2
3
4
5
steps:
- bash: |
echo Android sdk location: $ANDROID_SDK_ROOT
mkdir $ANDROID_SDK_ROOT/platform-tools/api/
cp $ANDROID_SDK_ROOT/platforms/android-30/data/api-versions.xml $ANDROID_SDK_ROOT/platform-tools/api/

0x05 Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.

升级android sdk api 版本到31,适配android 12 ,遇到这个问题。当前开发环境:

android studio 版本: 2020.3.1

AGP 版本: 4.1.2 (classpath "com.android.tools.build:gradle:4.1.2"

SDK 版本

1
2
3
4
5
6
7
8
9
android {
compileSdkVersion 31
buildToolsVersion '31.0.0'

defaultConfig {
minSdkVersion 21
targetSdkVersion 31
}
}

SDK Manager更新对应版本都正常下载,编译过程出现异常

1
Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.

是 Build Tools 升级之后,DX 变成了 D8。而 AGP 4.x 的版本使用的还是DX。

解决的方案:

1
2
3
4
5
# change below to your Android SDK path
cd ~/Library/Android/sdk/build-tools/31.0.0 \
&& mv d8 dx \
&& cd lib \
&& mv d8.jar dx.jar

C:\Users\user\AppData\Local\Android\Sdk\build-tools\31.0.0\d8.bat 改为 dx.bat
C:\Users\user\AppData\Local\Android\Sdk\build-tools\31.0.0\lib\d8.jar 改为 dx.jar

PS:也可以尝试升级 AGP 到 7.x

Problems专题:ViewPager2

ViewPager2

0x01 FragmentManager is already executing transactions

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
java.lang.IllegalStateException: FragmentManager is already executing transactions
at androidx.fragment.app.FragmentManager.ensureExecReady(FragmentManager.java:1778)
at androidx.fragment.app.FragmentManager.execSingleAction(FragmentManager.java:1814)
at androidx.fragment.app.BackStackRecord.commitNow(BackStackRecord.java:297)

at androidx.viewpager2.adapter.FragmentStateAdapter.removeFragment(FragmentStateAdapter.java:464)
at androidx.viewpager2.adapter.FragmentStateAdapter.gcFragments(FragmentStateAdapter.java:228)
at androidx.viewpager2.adapter.FragmentStateAdapter.restoreState(FragmentStateAdapter.java:569)
at androidx.viewpager2.widget.ViewPager2.restorePendingState(ViewPager2.java:350)
at androidx.viewpager2.widget.ViewPager2.dispatchRestoreInstanceState(ViewPager2.java:375)

at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3829)

at android.view.View.restoreHierarchyState(View.java:18613)

at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:573)
at androidx.fragment.app.FragmentStateManager.restoreViewState(FragmentStateManager.java:356)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1189)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1356)
at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1434)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1497)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2625)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2577)

at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:247)

at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:541)
at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1392)
at android.app.Activity.performStart(Activity.java:7260)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3009)

at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1840)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:6878)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

解决方案

如果在 Fragment 中使用 ViewPager2,那么 FragmentStateAdapter 应该使用 childFragmentManager。将

1
FragmentStateAdapter viewPagerAdapter = new FragmentStateAdapter(getActivity().getSupportFragmentManager(), titles);

改为

1
FragmentStateAdapter viewPagerAdapter = new FragmentStateAdapter(getChildFragmentManager(), titles);

0x02 ViewPager2+FragmentStateAdapter 的 notifyDataSetChanged 方法失效

原因分析

因为 FragmentStateAdapter 会保存所有 Fragment 实例,当调用 Adapter.notifyDataSetChanged() 方法时,Fragment 并没有走 onCreate 方法。

解决方案:

方案一(这个方法会导致内存泄漏,不推荐)

在调用 notifyDataSetChanged 之前,清空 FragmentStateAdapter 的 Fragment 列表。

方案二

重写 getItemId() containsItem() 这两个方法,并确保 getItemId() 的值是唯一的。

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
override fun createViewPagerAdapter(): RecyclerView.Adapter<*> {
val items = items // avoids resolving the ViewModel multiple times
return object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): PageFragment {
val itemId = items.itemId(position)
val itemText = items.getItemById(itemId)
return PageFragment.create(itemText)
}
override fun getItemCount(): Int = items.size
override fun getItemId(position: Int): Long = items.itemId(position)
override fun containsItem(itemId: Long): Boolean = items.contains(itemId)
}
}

/** A very simple collection of items. Optimized for simplicity (i.e. not performance). */
class ItemsViewModel : ViewModel() {
private var nextValue = 1L

private val items = (1..9).map { longToItem(nextValue++) }.toMutableList()

fun getItemById(id: Long): String = items.first { itemToLong(it) == id }
fun itemId(position: Int): Long = itemToLong(items[position])
fun contains(itemId: Long): Boolean = items.any { itemToLong(it) == itemId }
fun addNewAt(position: Int) = items.add(position, longToItem(nextValue++))
fun removeAt(position: Int) = items.removeAt(position)
fun createIdSnapshot(): List<Long> = (0 until size).map { position -> itemId(position) }
val size: Int get() = items.size

private fun longToItem(value: Long): String = "item#$value"
private fun itemToLong(value: String): Long = value.split("#")[1].toLong()
}

0x03 Design assumption violated

1
2
3
4
5
6
java.lang.IllegalStateException: Design assumption violated.
at androidx.viewpager2.widget.ViewPager2.updateCurrentItem(ViewPager2.java:538)
at androidx.viewpager2.widget.ViewPager2$4.onAnimationsFinished(ViewPager2.java:518)
at androidx.recyclerview.widget.RecyclerView$ItemAnimator.isRunning(RecyclerView.java:13244)
at androidx.viewpager2.widget.ViewPager2.onLayout(ViewPager2.java:515)
at android.view.View.layout(View.java:15596)

解决方案:

如果重写了 getItemId() containsItem() 这两个方法,确保 getItemId() 的值是唯一的。代码同 0x02

0x04 ViewPager2 嵌套 RecyclerView 手势冲突问题

img

原因分析:

同方向滚动事件被 ViewPager2 拦截了。

解决方案:

方案一 自定义 NestedScrollableHost

采用官方提供的自定义 NestedScrollableHost 来包一层 RecyclerView

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
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign

/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}

private val child: View? get() = if (childCount > 0) getChildAt(0) else null

init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}

private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}

override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}

private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return

// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}

if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

对应的 layout 代码:

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
... 水平
<androidx.viewpager2.integration.testapp.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/first_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF" />
</androidx.viewpager2.integration.testapp.NestedScrollableHost>
... 竖直
<androidx.viewpager2.integration.testapp.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="8dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/second_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF" />
</androidx.viewpager2.integration.testapp.NestedScrollableHost>

</LinearLayout>

方案二 自定义 RecyclerView

自定义 NestedRecyclerView 的分发事件通过 requestDisallowInterceptTouchEvent() 方法来限制父布类的拦截事件

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
public class NestedRecyclerView extends RecyclerView {
public NestedRecyclerView(@NonNull Context context) {
super(context);
}
public NestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public NestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

private int startX, startY;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getX();
int endY = (int) ev.getY();
int disX = Math.abs(endX - startX);
int disY = Math.abs(endY - startY);

if (disX > disY) {
getParent().requestDisallowInterceptTouchEvent(canScrollHorizontally(startX - endX));
} else {
getParent().requestDisallowInterceptTouchEvent(canScrollVertically(startX - endX));
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}

方法三 使用 ViewPager

降级,使用 ViewPager 来嵌套 RecyclerView ,可以避免事件冲突,亲测有效。

0x05 ViewPager2 两边保留上一页预览

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
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2

class PreviewPagesActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewpager2)
findViewById<ViewPager2>(R.id.view_pager).apply {
// Set offscreen page limit to at least 1, so adjacent pages are always laid out
offscreenPageLimit = 1
val recyclerView = getChildAt(0) as RecyclerView
recyclerView.apply {
clipToPadding = false
val leftPadding = resources.getDimensionPixelOffset(R.dimen.halfPageMargin) +
resources.getDimensionPixelOffset(R.dimen.peekOffset)
// setting padding on inner RecyclerView puts overscroll effect in the right place
// TODO: expose in later versions not to rely on
// getChildAt(0) which might break
setPadding(leftPadding, 0, leftPadding, 0)
}
adapter = Adapter()
}
}

class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_preview_pages, parent, false)
)

class Adapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemCount(): Int {
return 10
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ViewHolder(parent)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.tag = position
holder.itemView.setOnClickListener {
Toast.makeText(it.context, "position=$position", Toast.LENGTH_LONG).show()
}
}
}
}

0x06 Fragment no longer exists for key f1

在 Fragment 中使用 ViewPager 的时候,切换 Fragment 导致 ViewPager 无法正确恢复异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java.lang.IllegalStateException: Fragment no longer exists for key f1: unique id 55efaee5-a65c-4e57-9281-7c8f8f6e4156
at androidx.fragment.app.FragmentManager.getFragment(FragmentManager.java:960)
at androidx.fragment.app.FragmentStatePagerAdapter.restoreState(FragmentStatePagerAdapter.java:328)
at androidx.viewpager.widget.ViewPager.onRestoreInstanceState(ViewPager.java:1461)
at android.view.View.dispatchRestoreInstanceState(View.java:20032)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3922)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:3928)
at android.view.View.restoreHierarchyState(View.java:20010)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:639)
at androidx.fragment.app.Fragment.restoreViewState(Fragment.java:3010)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:3001)
at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:580)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:285)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:230)
at android.app.ActivityThread.main(ActivityThread.java:8018)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:526)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)

20240301183554

在这个页面中,内容列表使用 ViewPager 嵌套 Fragment 实现,并和时间选择 Tab 绑定。切换【即将上线】和【播出时间表】Tab,实际是使用 FragmentManager 的 replace 方法,动态切换两个 Fragment,然后就报了上面的异常。

网上流行的解决方案是使用 FragmentPagerAdapter 或者添加

1
2
3
4
@Override
public Parcelable saveState() {
return null;
}

但是这样处理会导致 ViewPager 中的 fragments 全部无法恢复,导致 ViewPager 白屏。

本例中的解决方案是:

在切换【即将上线】和【播出时间表】Tab 时,不使用 FragmentManager 的 replace 方法,而采用动态的 Hide 和 show 方法暂时规避 Fragment 被回收的问题。

Problems专题:RecyclerView

Problems专题:RecyclerView

0x01 Called attach on a child which is not detached

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
java.lang.IllegalArgumentException: Called attach on a child which is not detached: BaseViewHolder{2b241e1 position=12 id=-1, oldPos=-1, pLpos:-1} androidx.recyclerview.widget.RecyclerView{afecb06 VFED..... ......ID 0,0-1080,2055 #7f09236e app:id/recycler_view_xxx}, adapter:com.xxxx.adapter.XxxAdapter@cfc75c7, layout:androidx.recyclerview.widget.LinearLayoutManager@24af7f4, context:com.xxxx.XxxActivity@1ed75e2
at androidx.recyclerview.widget.RecyclerView$5.attachViewToParent(RecyclerView.java:917)
at androidx.recyclerview.widget.ChildHelper.attachViewToParent(ChildHelper.java:239)
at androidx.recyclerview.widget.RecyclerView.addAnimatingView(RecyclerView.java:1438)
at androidx.recyclerview.widget.RecyclerView.animateDisappearance(RecyclerView.java:4377)
at androidx.recyclerview.widget.RecyclerView$4.processDisappeared(RecyclerView.java:616)
at androidx.recyclerview.widget.ViewInfoStore.process(ViewInfoStore.java:242)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4210)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3864)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4410)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1131)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at androidx.viewpager.widget.ViewPager.onLayout(ViewPager.java:1775)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1131)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at com.android.internal.policy.DecorView.onLayout(DecorView.java:905)
at android.view.View.layout(View.java:22213)
at android.view.ViewGroup.layout(ViewGroup.java:6340)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3286)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2757)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1865)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7933)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1018)
at android.view.Choreographer.doCallbacks(Choreographer.java:837)
at android.view.Choreographer.doFrame(Choreographer.java:767)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1003)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:230)
at android.app.ActivityThread.main(ActivityThread.java:7951)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:526)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)

问题分析

对同一个 position 位置同时进行notifyItemRemoved(position)notifyItemInserted(position) 操作导致。

解决方案

避免同时对同一个位置先 notifyItemRemoved 再 notifyItemInserted,使用 notifyItemChanged。

1
adapter?.notifyItemChanged(position)

0x02 RecyclerView设置最大高度、宽度

当RecyclerView属性设置为wrap_content+maxHeight时,maxHeight没有效果。

问题分析

当RecyclerView的LayoutManager#isAutoMeasureEnabled()返回true时,RecyclerView高度取决于children view的布局高度,并非取决于RecyclerView自身的测量高度。

解决方案

因此,我们只需要重写LayoutManager的public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)方法即可为RecyclerView设置最大宽高。

1
2
3
4
5
6
7
8
9
recyclerView.layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, false) {
override fun setMeasuredDimension(childrenBounds: Rect?, wSpec: Int, hSpec: Int) {
val height = View.MeasureSpec.getSize(hSpec)
val maxHeight = getScreenHeight() * 4 / 5
val realHeight = height.coerceAtMost(maxHeight)
val realHeightSpec = View.MeasureSpec.makeMeasureSpec(realHeight, AT_MOST)
super.setMeasuredDimension(childrenBounds, wSpec, realHeightSpec)
}
}

作者:猫爸iYao
链接:https://www.jianshu.com/p/0dec79ff70df
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Problems专题:Parcel

Problems专题:Parcel

0x01 Unmarshalling unknown type

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
Thread Name: 'main' 
java.lang.RuntimeException: Parcel android.os.Parcel@bbfcc04: Unmarshalling unknown type code 2131296928 at offset 1088
at android.os.Parcel.readValue(Parcel.java:2750)
at android.os.Parcel.readSparseArrayInternal(Parcel.java:3126)
at android.os.Parcel.readSparseArray(Parcel.java:2354)
at android.os.Parcel.readValue(Parcel.java:2728)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3045)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:288)
at android.os.BaseBundle.unparcel(BaseBundle.java:232)
at android.os.Bundle.getSparseParcelableArray(Bundle.java:1010)
at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2133)
at android.app.Activity.onRestoreInstanceState(Activity.java:1173)
at android.app.Activity.performRestoreInstanceState(Activity.java:1128)
at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1318)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3025)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1840)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:6878)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

问题分析

情况1 Parcelable 对象为空,反序列化异常

情况2 Parcelable 序列化和反序列化的字段和顺序没有完全对应

情况3 自定义View的数据保存与恢复

解决方案

情况1 Parcelable 对象在序列化和反序列化增加 null 值判断

情况2 Parcelable 对象 read 和 write 字段的类型和顺序保持一直

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Account implements Parcelable {
public Account(Parcel in) {
this.name = in.readString();
this.type = in.readInt();
if (TextUtils.isEmpty(name)) {
throw new android.os.BadParcelableException("the name must not be empty: " + name);
}
// ...
this.accessId = in.readString();
// ...
}

public int describeContents() {
return 0;
}

public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(type);
dest.writeString(accessId);
}
// ...
}

情况3 自定义View的数据保存与恢复

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
override fun onSaveInstanceState(): Parcelable? {
Log.d("NavigationBar", "onSaveInstanceState: selectedId=${mSelectedId}")
return SavedState(super.onSaveInstanceState(), mSelectedId)
}

override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
val id = state.selectedId
Log.d("NavigationBar", "onRestoreInstanceState: selectedId=${state.selectedId}")
super.onRestoreInstanceState(state.superState)
select(id)
return
}
return super.onRestoreInstanceState(state)
}

internal class SavedState : BaseSavedState {
var selectedId: Int = View.NO_ID

constructor(source: Parcel) : super(source) {
selectedId = source.readInt()
Log.d("NavigationBar", "readFromParcel: selectedId=$selectedId")
}

constructor(superState: Parcelable?, selectedId: Int) : super(superState) {
this.selectedId = selectedId
}

override fun writeToParcel(parcel: Parcel, flags: Int) {
super.writeToParcel(parcel, flags)
parcel.writeInt(selectedId)
Log.d("NavigationBar", "writeToParcel: selectedId=$selectedId")
}

override fun describeContents(): Int {
return 0
}

companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}

override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}

0x02 android.os.TransactionTooLargeException

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
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 542688 bytes
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:160)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:6878)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
Caused by: android.os.TransactionTooLargeException: data parcel size 542688 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:479)
at android.app.IActivityManager$Stub$Proxy.activityStopped(IActivityManager.java:3941)
at java.lang.reflect.Method.invoke(Native Method)
at com.taobao.monitor.impl.common.c.invoke(ActivityManagerHook.java:89)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy2.activityStopped(Unknown Source)
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:144)
... 7 more
android.os.TransactionTooLargeException: data parcel size 542688 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:479)
at android.app.IActivityManager$Stub$Proxy.activityStopped(IActivityManager.java:3941)
at java.lang.reflect.Method.invoke(Native Method)
at com.taobao.monitor.impl.common.c.invoke(ActivityManagerHook.java:89)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy2.activityStopped(Unknown Source)
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:144)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:6878)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

问题分析

1 Intent 传递的数据过大。

2 onSaveInstance 保存的数据过大。

解决方案

尽可能的使用少量的数据。

大数据考虑持久化和其他传递形式。

Problems专题:Dialog

Problems专题:Dialog

0x01 OnKeyDown部分机型无法监听

问题分析

监听返回键和音量键,重载OnKeyDown()方法,部分机型会失效。

解决方案

给相应的Dialog监听setOnKeyListener()。

1
2
3
4
5
6
7
8
9
10
// 解决不同机型版本兼容问题,onKeyDown 可能被拦截
setOnKeyListener { dialog, keyCode, event ->
Log.d(TAG, "setOnKeyListener: keyCode = $keyCode, event = $event")
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
handleKeyEvent(keyCode)
true
} else {
false
}
}

注意区分keycode,防止业务层重复处理

0x02 DialogFragment不能自动弹出软键盘

方案一:延迟弹出软键盘
在dialog显示之后,延迟200ms再显示软键盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//强制显示或者关闭系统键盘
public static void toggleKeyboard(final EditText editText, final boolean status) {
if (editText == null) return;
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
InputMethodManager m = (InputMethodManager)
ApplicationExtKt.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (status) {
m.showSoftInput(editText, InputMethodManager.SHOW_FORCED);
} else {
IBinder windowToken = editText.getWindowToken();
if (windowToken != null) {
m.hideSoftInputFromWindow(windowToken, 0);
}
}
}
}, status ? 200 : 100);
}

方案二:设置 SoftInputMode 为 SOFT_INPUT_STATE_ALWAYS_VISIBLE

1
2
getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
inputEditText.requestFocus();

0x03 关闭DialogFragment无法关闭软键盘

问题分析

一般情况下,在onPause或者dismiss方法直接调用hideKeyboard就可以

1
2
3
4
override fun onPause() {
KeyBoardUtils.hideKeyboard(binding.etSearch)
super.onPause()
}

但是,在某些时候还是会存在关闭不成功的情况。这是由于Dialog下面的Activity或Fragment存在EditText等抢占焦点,导致在DialogFragment在调用dismiss方法时,键盘已经被抢占焦点,所以无法关闭。

解决方案

在DialogFragment的dismiss方法回调

1
2
3
4
override fun onDismiss(dialog: DialogInterface) {
listener?.onDialogDismiss()
super.onDismiss(dialog)
}

在前一个Activity或者Fragment中重新关闭键盘。

1
2
3
4
5
6
7
// 消除弹框遗留下来的keyboard
private fun onDialogDismiss() {
// 消除弹框
Handler().postDelayed({
KeyBoardUtils.hideKeyboard(binding.root)
}, 200)
}
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×