Gson 数据解析

Gson 数据解析

0x01 Kotlin Gson 解析 data class 两条黄金法则:

1、 String 必须是可空类型 String?

2、 需要使用默认值,则全部字段都必须给予默认值,以满足kotlin对象有空的构造函数

0x02 手动解析Gson基础字段

1、msg 可空String解析 jsonReader.peek() == JsonToken.NULL

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
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import okhttp3.ResponseBody
import retrofit2.Converter
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class ResponseBodyConverter(val gson: Gson, val type: Type) :
Converter<ResponseBody, BaseResponse<Any>> {
override fun convert(value: ResponseBody): BaseResponse<Any> {
val dataType = GenericsUtils.getParameterUpperBound(
0,
type as ParameterizedType
)
val baseResponse = BaseResponse<Any>()
val jsonReader = JsonReader(value.charStream())
try {
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val name: String = jsonReader.nextName()
when (name) {
"code" -> {
baseResponse.code = jsonReader.nextString()
}
"msg" -> {
// this works, but do not do this.
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull()
baseResponse.msg = null
} else {
baseResponse.msg = jsonReader.nextString()
}
}
"data" -> {
val mapped: TypeAdapter<*>? = gson.getAdapter(TypeToken.get(dataType))
baseResponse.data = mapped?.read(jsonReader)
}
else -> {
jsonReader.skipValue()
}
}
}
} catch (e: IllegalStateException) {
throw JsonSyntaxException(e)
} catch (e: IllegalAccessException) {
throw AssertionError(e)
}
jsonReader.endObject()
return baseResponse
}
}

大量文本的浏览进度和浏览时长统计

大量文本的浏览进度和浏览时长统计

埋点需求,Android App 需要在onResume 和 onPause 方法中计算浏览的时长,同时上报浏览的进度。

浏览进度Rate具体的计算方式的具体过程:

1、在滚动屏幕过程中,通过 textContent?.viewTreeObserver?.addOnScrollChangedListener 来记录屏幕滚动的位置

2、在滚动监听里通过textContent?.getLocationOnScreen(location)获取在屏幕的具体位置,同时,

计算出visibleHeight = screenHeight - location[1] 当前文本的可见高度

3、当可见高度超过目标的高度,则认为已经全部浏览,rate = 100% ,同时移除滚动监听textContent?.viewTreeObserver?.removeOnScrollChangedListener(mScrollChangeListener)

核心代码如下:

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
private var totalHeight: Int = 0
private var visibleHeight: Int = 0
private var screenHeight = 0

fun getViewRate(): String? =
if (totalHeight <= 0 || visibleHeight < 0) null
else "${(visibleHeight * 100 / totalHeight)}%"

private fun bindViewTreeObserver() {
cleanCache()
contentTv.post {
totalHeight = contentTv.height
screenHeight = ScreenUtils.getScreenHeight(context)
contentVisibleRate()
}
contentTv.viewTreeObserver.addOnScrollChangedListener(mScrollChangeListener)
}

private fun cleanCache() {
totalHeight = 0
screenHeight = 0
visibleHeight = 0
}

private val mScrollChangeListener = ViewTreeObserver.OnScrollChangedListener {
contentVisibleRate()
}

private fun contentVisibleRate() {
val location = IntArray(2)
contentTv.getLocationOnScreen(location)
visibleHeight = (screenHeight - location[1]).coerceAtLeast(visibleHeight)
if (visibleHeight >= totalHeight) {
visibleHeight = totalHeight
removeOnScrollChangedListener()
}
}

private fun removeOnScrollChangedListener() {
mScrollChangeListener?.let {
contentTv.viewTreeObserver.removeOnScrollChangedListener(mScrollChangeListener)
}
}

RecyclerViewHelper

RecyclerViewHelper

提供了注册加载更多,和判断是否不足一屏等工具方法

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
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager

object RecyclerViewHelper {
/**
* RecyclerView 注册加载更多监听
*/
@JvmStatic
fun addOnScrollListener(recyclerView: RecyclerView, loadMore: () -> Unit) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
val count = layoutManager.itemCount
if (lastPosition >= count - 2) {
loadMore()
return@onScrollStateChanged
}
} else if (layoutManager is StaggeredGridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val result = IntArray(spanCount)
layoutManager.findLastCompletelyVisibleItemPositions(result)
for (it in result) {
if (it >= count - spanCount - 1) {
loadMore()
return@onScrollStateChanged
}
}
} else if (layoutManager is GridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
if (lastPosition >= count - spanCount - 1) {
loadMore()
return@onScrollStateChanged
}

}
}
}
})
}


/**
* 判断是否一屏显示
*
* 错误或者空了返回 false
*/
@JvmStatic
fun isOneScreen(recyclerView: RecyclerView?): Boolean {
recyclerView?.let {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
// include GridLayoutManager
val count = layoutManager.itemCount
return count > 0 &&
layoutManager.findFirstCompletelyVisibleItemPosition() == 0 &&
layoutManager.findLastCompletelyVisibleItemPosition() == count - 1
} else if (layoutManager is StaggeredGridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val last = IntArray(spanCount)
val first = IntArray(spanCount)
layoutManager.findLastCompletelyVisibleItemPositions(last)
layoutManager.findFirstCompletelyVisibleItemPositions(first)
return count > 0 && first.min() == 0 && last.max() == count - 1
}
}

return false
}

private fun IntArray.min(): Int {
if (this.isNotEmpty()) {
var result = this[0]
this.forEach {
if (result > it) result = it
}
return result
}
return -1
}

private fun IntArray.max(): Int {
if (this.isNotEmpty()) {
var result = this[0]
this.forEach {
if (result < it) result = it
}
return result
}
return -1
}
}

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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Your browser is out-of-date!

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

×