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 保存的数据过大。

解决方案

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

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

链表相交

编写一个程序,找到两个单链表相交的起始节点。

例如,下面的两个链表:

1
2
3
4
5
A:          a1 → a2

c1 → c2 → c3

B: b1 → b2 → b3

在节点 c1 开始相交。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

方法一:哈希集合

思路和算法

判断两个链表是否相交,可以使用哈希集合存储链表节点。

首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:

  • 如果当前节点不在哈希集合中,则继续遍历下一个节点;

  • 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。

如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。

复杂度分析

时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。需要遍历两个链表各一次。

空间复杂度:O(m),其中 m 是链表 headA 的长度。需要使用哈希集合存储链表 headA 中的全部节点。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> visited = new HashSet<ListNode>();
ListNode temp = headA;
while (temp != null) {
visited.add(temp);
temp = temp.next;
}
temp = headB;
while (temp != null) {
if (visited.contains(temp)) {
return temp;
}
temp = temp.next;
}
return null;
}
}

方法二:双指针

思路和算法

使用双指针的方法,可以将空间复杂度降至 O(1)。

只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。

当链表 headA 和 headB 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:

  • 每步操作需要同时更新指针 pA 和 pB;

  • 如果指针 pA 不为空,则将指针 pA 移到下一个节点;如果指针 pB 不为空,则将指针 pB 移到下一个节点。

  • 如果指针 pA 为空,则将指针 pA 移到链表 headB 的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。

  • 当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 null。

证明

考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。

复杂度分析

时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。两个指针同时遍历两个链表,每个指针遍历两个链表各一次。

空间复杂度:O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}

参考连接:

https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/xiang-jiao-lian-biao-by-leetcode-solutio-a8jn/

https://zhuanlan.zhihu.com/p/48313122

RecyclerView Item 嵌套 ScrollView

RecyclerView Item 嵌套 ScrollView

RecyclerView Item 嵌套 ScrollView 产生 Touch 事件冲突,通过自定义ScrollView来拦截和处理事件

image-20210908211849925

自定义 ItemScrollView 代码如下

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
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ScrollView

class ItemScrollView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ScrollView(context, attrs, defStyleAttr) {

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
return super.onInterceptTouchEvent(ev)
}

private var lastY: Float = 0f

override fun onTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
lastY = ev.y
}

MotionEvent.ACTION_MOVE -> {
val currentY = ev.y
this.scrollBy(0, (lastY - currentY).toInt())
lastY = currentY
}

MotionEvent.ACTION_UP -> {
lastY = 0f
}
}

return canScroll()
}

private fun canScroll(): Boolean {
val child = getChildAt(0)
child?.let {
return height < child.height
}
return false
}
}

PowerShell最佳实践

PowerShell最佳实践

Windows 10 系统自带 PowerShell,美化教程

image-20210909150114264

0x01 安装Fluent Terminal

在Windows 应用商店安装,或者Github

0x02 安装powershell模块

1、安装posh-git、oh-my-posh和Get-ChildItemColor(美化ls命令):

在powershell管理员模式下:

1
2
3
$ Install-Module posh-git -Scope CurrentUser
$ Install-Module oh-my-posh -Scope CurrentUser
$ Install-Module DirColors

2、设置修改powershell的配置文件:

1
2
$ if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force }
$ notepad $PROFILE

输入内容:

1
2
3
4
Import-Module DirColors
Import-Module posh-git
Import-Module oh-my-posh
Set-PoshPrompt -Theme PowerLine

其中主题名可以在下面的路径里找到,可以自行切换主题。

1
C:\Program Files\WindowsPowerShell\Modules\oh-my-posh\3.163.0\themes

0x03 安装Powerline字体

在Fluent Terminal设置 powerline 字体和字体大小。

0x04 文件管理器命令

在文件夹中打开:

1
2
3
4
5
6
7
8
# I 在文件夹中打开
$ explorer (gl)
# II 在文件夹中打开
$ start .
# III 在文件夹中打开
$ ii .
# 打开当前根目录
$ ii /

在当前文件夹打开PowerShell:

空白处 Shift + 鼠标右键,在弹出的菜单中选择PowerShell.

0x05 VS Code 命令

1
$ code .
Your browser is out-of-date!

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

×