From 3369bf6c89c091a316932d41349dddc01e0b4e73 Mon Sep 17 00:00:00 2001 From: dench Date: Mon, 11 Mar 2024 22:45:01 +0800 Subject: [PATCH] Site updated: 2024-03-11 22:45:00 --- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../Git\346\211\213\345\206\214/index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../vim\345\205\245\351\227\250/index.html" | 2 +- 2020/02/17/VimTutor/index.html | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../Implementation\345\222\214api/index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- 2021/05/19/BottomFragment/index.html | 2 +- 2021/05/21/MessageCenter/index.html | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 44 ++++--- .../index.html" | 2 +- 2021/08/23/RecyclerViewHelper/index.html | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 112 ++++++++++++++++++ 404/index.html | 2 +- about/index.html | 2 +- archives/2020/02/index.html | 2 +- archives/2020/03/index.html | 2 +- archives/2020/08/index.html | 2 +- archives/2020/09/index.html | 2 +- archives/2020/12/index.html | 2 +- archives/2020/index.html | 2 +- archives/2021/05/index.html | 2 +- archives/2021/06/index.html | 2 +- archives/2021/08/index.html | 2 +- archives/2021/09/index.html | 2 +- archives/2021/11/index.html | 2 +- archives/2021/12/index.html | 2 +- archives/2021/index.html | 2 +- archives/2022/01/index.html | 2 +- archives/2022/02/index.html | 2 +- archives/2022/03/index.html | 2 +- archives/2022/04/index.html | 2 +- archives/2022/06/index.html | 2 +- archives/2022/08/index.html | 2 +- archives/2022/09/index.html | 2 +- archives/2022/10/index.html | 2 +- archives/2022/11/index.html | 2 +- archives/2022/12/index.html | 2 +- archives/2022/index.html | 2 +- archives/2023/01/index.html | 2 +- archives/2023/02/index.html | 2 +- archives/2023/03/index.html | 2 +- archives/2023/05/index.html | 2 +- archives/2023/06/index.html | 2 +- archives/2023/07/index.html | 2 +- archives/2023/08/index.html | 2 +- archives/2023/09/index.html | 2 +- archives/2023/10/index.html | 2 +- archives/2023/11/index.html | 2 +- archives/2023/12/index.html | 2 +- archives/2023/index.html | 2 +- archives/2024/03/index.html | 86 ++++++++++++++ archives/2024/index.html | 86 ++++++++++++++ archives/index.html | 2 +- archives/page/2/index.html | 2 +- categories/index.html | 4 +- content.json | 2 +- index.html | 40 +++++-- page/10/index.html | 11 +- page/11/index.html | 19 ++- page/12/index.html | 14 ++- page/13/index.html | 9 +- page/14/index.html | 11 +- page/15/index.html | 15 +-- page/16/index.html | 13 +- page/2/index.html | 22 ++-- page/3/index.html | 15 ++- page/4/index.html | 15 +-- page/5/index.html | 15 ++- page/6/index.html | 29 +---- page/7/index.html | 33 ++++-- page/8/index.html | 69 +++++------ page/9/index.html | 28 ++++- tags/AIGC/index.html | 112 ++++++++++++++++++ tags/Android/index.html | 42 ++++--- tags/Android/page/2/index.html | 2 +- tags/Aria/index.html | 2 +- tags/ExoPlayer/index.html | 2 +- tags/FileProvider/index.html | 2 +- tags/Git/index.html | 2 +- tags/Gradle/index.html | 2 +- tags/Gson/index.html | 2 +- tags/H5/index.html | 2 +- tags/Hexo/index.html | 2 +- tags/Homebrew/index.html | 2 +- tags/Java/index.html | 2 +- tags/JsBridge/index.html | 2 +- tags/Kotlin/index.html | 2 +- tags/Netty/index.html | 2 +- tags/NexT/index.html | 2 +- .../index.html" | 42 ++++--- tags/Proxy/index.html | 2 +- tags/Python/index.html | 2 +- tags/Reading/index.html | 2 +- tags/RecyclerView/index.html | 2 +- tags/SSH/index.html | 2 +- tags/Shell/index.html | 2 +- tags/Shortcuts/index.html | 2 +- tags/Vim/index.html | 2 +- tags/adb/index.html | 2 +- tags/iTerm2/index.html | 2 +- tags/index.html | 4 +- tags/stable-diffusion/index.html | 112 ++++++++++++++++++ "tags/\344\270\213\350\275\275/index.html" | 2 +- .../index.html" | 2 +- "tags/\346\272\220\347\240\201/index.html" | 2 +- "tags/\347\256\227\346\263\225/index.html" | 2 +- 189 files changed, 947 insertions(+), 381 deletions(-) create mode 100644 "2024/03/07/Windows\351\203\250\347\275\262stable-diffusion/index.html" create mode 100644 archives/2024/03/index.html create mode 100644 archives/2024/index.html create mode 100644 tags/AIGC/index.html create mode 100644 tags/stable-diffusion/index.html diff --git "a/2020/02/04/\345\246\202\344\275\225\351\230\205\350\257\273\344\270\200\346\234\254\344\271\246/index.html" "b/2020/02/04/\345\246\202\344\275\225\351\230\205\350\257\273\344\270\200\346\234\254\344\271\246/index.html" index eaa97eb3..853db7d5 100644 --- "a/2020/02/04/\345\246\202\344\275\225\351\230\205\350\257\273\344\270\200\346\234\254\344\271\246/index.html" +++ "b/2020/02/04/\345\246\202\344\275\225\351\230\205\350\257\273\344\270\200\346\234\254\344\271\246/index.html" @@ -107,7 +107,7 @@

作者

Dench

发布于

2020-02-04

更新于

2020-02-04

许可协议

CC BY-NC-SA 4.0

Python爬取网络图片

直接上源码

-
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
'''
用Python爬某新闻网站的照片
PS:仅测试爬虫功能,滥用后果自负
'''
import requests
from hashlib import md5
import re
import os

WEB_URL = 'https://new.qq.com/omn/20200207/20200207A0OSQX00.html'
IMAGE_PATH='/Users/**/workspaces/pythonP/img1'

# 通过正则找出网页中所有的图片地址
def find_img_url():
r = requests.get(WEB_URL)
r.raise_for_status()
r.encoding = r.apparent_encoding
demo = r.text
list = []
# ".*?" :正则表达式匹配任意字符串
pattern1 = '<img src=".*?" class="content-picture">'
results1 = re.findall(pattern1, demo)
for res1 in results1:
# <img src="//inews.gtimg.com/newsapp_bt/0/11299639206/1000" class="content-picture">
# https://inews.gtimg.com/newsapp_bt/0/11299639205/1000
res1 = res1.replace('<img src=\"','https:')
res1 = res1.replace('\" class=\"content-picture\">','')
list.append(res1)
# print(res1)
pattern2 = '\"http://inews.gtimg.com/newsapp_bt/0/.*?/1000\"'
results2 = re.findall(pattern2, demo)
for res2 in results2:
res2 = res2.replace('\"','')
list.append(res2)
# print(res2)
return list

# 下载图片到本地
def download_image(img_url):
r = requests.get(img_url)
r.raise_for_status()
content = r.content
file_path = '{0}/{1}.{2}'.format(IMAGE_PATH, md5(content).hexdigest(), 'jpg')
# print(file_path)
if not os.path.exists(file_path):#os.path.exists(file_path)判断文件是否存在,存在返回1,不存在返回0
with open(file_path, 'wb') as f:
f.write(content)
f.close()

list = find_img_url()
for i in list:
print(i, ' download...')
download_image(i)
print("一共下载{}条数据!".format(len(list)))

作者

Dench

发布于

2020-02-08

更新于

2020-02-08

许可协议

CC BY-NC-SA 4.0

BottomFragment

底部弹出控件 - Fragment 实现

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
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.dench.baselib.R
import com.dench.baselib.databinding.FragmentBottomBinding

class BottomFragment : Fragment() {
companion object {
fun start(fm: FragmentManager, fragment: Fragment): BottomFragment {
val bottomFragment = BottomFragment().apply {
setFragment(fragment)
}
fm.beginTransaction()
.setCustomAnimations(
R.anim.fragment_bottom_enter,
0,
0,
R.anim.fragment_bottom_exit
)
.add(android.R.id.content, bottomFragment)
.addToBackStack(null)
.commitAllowingStateLoss()
return bottomFragment
}
}

private lateinit var fragment: Fragment

private fun setFragment(fragment: Fragment) {
this.fragment = fragment
}

private lateinit var binding: FragmentBottomBinding
private val TAG = "BottomFragment"

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentBottomBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.run {
binding.bottomFragmentRl.setOnClickListener(View.OnClickListener {
Log.d(TAG, "bottom root view click.")
dismissSelf()
})

/** add fragment */
childFragmentManager.beginTransaction()
.add(R.id.container, fragment)
.commitAllowingStateLoss()
}
}

private fun dismissSelf() {
parentFragmentManager.popBackStack()
}
}
-
作者

Dench

发布于

2021-05-19

更新于

2021-05-19

许可协议

CC BY-NC-SA 4.0

Android专栏-BaseQuickAdapterHelper

Android专栏-BaseQuickAdapterHelper

0x01 自动加载更多-LoadingFooterView

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
import android.view.View
import android.view.ViewGroup
import com.chad.library.adapter.base.loadmore.BaseLoadMoreView
import com.chad.library.adapter.base.loadmore.LoadMoreStatus
import com.chad.library.adapter.base.util.getItemView
import com.chad.library.adapter.base.viewholder.BaseViewHolder

class LoadingFooterView : BaseLoadMoreView() {
private var loadingView: LoadingPagView? = null
override fun getRootView(parent: ViewGroup): View {
val rootView = parent.getItemView(R.layout.ui_footer_adapter_load_more)
loadingView = rootView.findViewById(R.id.loadingView)
return rootView
}

override fun getLoadingView(holder: BaseViewHolder): View {
return holder.getView(R.id.loadingView)
}

override fun getLoadComplete(holder: BaseViewHolder): View {
return holder.getView(R.id.fakeView)
}

override fun getLoadEndView(holder: BaseViewHolder): View {
return holder.getView(R.id.endView)
}

override fun getLoadFailView(holder: BaseViewHolder): View {
return holder.getView(R.id.fakeView)
}

override fun convert(holder: BaseViewHolder, position: Int, loadMoreStatus: LoadMoreStatus) {
super.convert(holder, position, loadMoreStatus)
when (loadMoreStatus) {
LoadMoreStatus.Complete -> {
loadingView?.stopPlay()
}
LoadMoreStatus.Loading -> {
loadingView?.startPlay()
}
LoadMoreStatus.Fail -> {

}
LoadMoreStatus.End -> {
loadingView?.stopPlay()
}
}
}
}
作者

Dench

发布于

2021-05-25

更新于

2021-05-25

许可协议

CC BY-NC-SA 4.0

Android专栏-BaseQuickAdapterHelper

Android专栏-BaseQuickAdapterHelper

0x01 自动加载更多-LoadingFooterView

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
import android.view.View
import android.view.ViewGroup
import com.chad.library.adapter.base.loadmore.BaseLoadMoreView
import com.chad.library.adapter.base.loadmore.LoadMoreStatus
import com.chad.library.adapter.base.util.getItemView
import com.chad.library.adapter.base.viewholder.BaseViewHolder

class LoadingFooterView : BaseLoadMoreView() {
private var loadingView: LoadingPagView? = null
override fun getRootView(parent: ViewGroup): View {
val rootView = parent.getItemView(R.layout.ui_footer_adapter_load_more)
loadingView = rootView.findViewById(R.id.loadingView)
return rootView
}

override fun getLoadingView(holder: BaseViewHolder): View {
return holder.getView(R.id.loadingView)
}

override fun getLoadComplete(holder: BaseViewHolder): View {
return holder.getView(R.id.fakeView)
}

override fun getLoadEndView(holder: BaseViewHolder): View {
return holder.getView(R.id.endView)
}

override fun getLoadFailView(holder: BaseViewHolder): View {
return holder.getView(R.id.fakeView)
}

override fun convert(holder: BaseViewHolder, position: Int, loadMoreStatus: LoadMoreStatus) {
super.convert(holder, position, loadMoreStatus)
when (loadMoreStatus) {
LoadMoreStatus.Complete -> {
loadingView?.stopPlay()
}
LoadMoreStatus.Loading -> {
loadingView?.startPlay()
}
LoadMoreStatus.Fail -> {

}
LoadMoreStatus.End -> {
loadingView?.stopPlay()
}
}
}
}
作者

Dench

发布于

2021-05-25

更新于

2021-05-25

许可协议

CC BY-NC-SA 4.0

自定义布局:西部世界 第一季

自定义布局:西部世界 第一季

这个自定义布局要求显示为 系列名称... + 第一季 ,后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示。

-
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
/**
* 系列名称... + 第一季
* 后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示
*/
class FixedEndLinearLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//获取父布局测量size和model
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
if (childCount != 2) throw RuntimeException("FixedEndLinearLayout must have 2 children.")
val wrapChild = getChildAt(0)
val fixedChild = getChildAt(1)

//测量
measureChild(fixedChild, widthMeasureSpec, heightMeasureSpec)
val fixedParams = fixedChild.layoutParams as MarginLayoutParams

val fixedChildWidth =
fixedChild.measuredWidth + fixedParams.leftMargin + fixedParams.rightMargin
val fixedChildHeight =
fixedChild.measuredHeight + fixedParams.topMargin + fixedParams.bottomMargin

val wrapChildWidthSpec = ViewGroup.getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight + fixedChildWidth, wrapChild.layoutParams.width
)
val wrapChildHeightSpec = ViewGroup.getChildMeasureSpec(
heightMeasureSpec, paddingTop + paddingBottom, wrapChild.layoutParams.height
)
wrapChild.measure(wrapChildWidthSpec, wrapChildHeightSpec)

val wrapParams = wrapChild.layoutParams as MarginLayoutParams

val wrapChildWidth =
wrapChild.measuredWidth + wrapParams.leftMargin + wrapParams.rightMargin
val wrapChildHeight =
wrapChild.measuredHeight + wrapParams.topMargin + wrapParams.bottomMargin

val width = wrapChildWidth + fixedChildWidth
val height = fixedChildHeight.coerceAtLeast(wrapChildHeight)

start0 = paddingLeft + wrapParams.leftMargin
start1 = paddingLeft + wrapChildWidth + fixedParams.leftMargin

setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) widthSize else width + paddingLeft + paddingRight,
if (heightMode == MeasureSpec.EXACTLY) heightSize else height + paddingTop + paddingBottom
)

}

private var start0 = 0
private var start1 = 0

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val wrapChild = getChildAt(0)
val fixedChild = getChildAt(1)
val y0 = (measuredHeight - wrapChild.measuredHeight) / 2
val y1 = (measuredHeight - fixedChild.measuredHeight) / 2
wrapChild.layout(
start0,
y0,
start0 + wrapChild.measuredWidth,
y0 + wrapChild.measuredHeight
)
fixedChild.layout(
start1,
y1,
start1 + fixedChild.measuredWidth,
y1 + fixedChild.measuredHeight
)
}
}
作者

Dench

发布于

2021-06-08

更新于

2021-06-08

许可协议

CC BY-NC-SA 4.0

RecyclerView的几种Decoration

RecyclerView的几种Decoration

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
import android.content.res.Resources
import android.graphics.Rect
import android.util.TypedValue
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class SimplePaddingDecoration(
spaceDp: Int,
val orientation: Int = RecyclerView.VERTICAL
) : RecyclerView.ItemDecoration() {
private val dividerHeight: Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
spaceDp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition

if (orientation == RecyclerView.VERTICAL) {
// 竖直
if (position + 1 != parent.adapter?.itemCount) {
outRect.set(0, 0, 0, dividerHeight)
} else {
outRect.set(0, 0, 0, 0)
}
} else {
// 水平
if (position + 1 != parent.adapter?.itemCount) {
outRect.set(0, 0, dividerHeight, 0)
} else {
outRect.set(0, 0, 0, 0)
}
}
}
}
-
作者

Dench

发布于

2021-06-17

更新于

2021-06-17

许可协议

CC BY-NC-SA 4.0

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)
+

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。将

@@ -42,40 +42,48 @@

改为

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

0x02 ViewPager2+FragmentStateAdapter的notifyDataSetChanged方法失效

原因分析

-

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

+

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 +

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

+

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

img

原因分析:

-

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

+

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

解决方案:

-

方案一 自定义NestedScrollableHost

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

+

方案一 自定义 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代码:

+

对应的 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);
}
}
+

方案二 自定义 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)
-

方法三 使用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()
}
}
}
}
+

20240301183554

+

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

+

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

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

Dench

发布于

2021-06-20

更新于

2021-06-20

许可协议

CC BY-NC-SA 4.0

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
}
}
作者

Dench

发布于

2021-08-23

更新于

2021-08-23

许可协议

CC BY-NC-SA 4.0

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
}
}
-
作者

Dench

发布于

2021-09-13

更新于

2021-09-13

许可协议

CC BY-NC-SA 4.0

SettingsHelper

SettingsHelper

SettingsHelper 自启动设置

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log

/**
* 跳转自启动页面
*/
object AutoStartHelper {
private val hashMap = mutableMapOf<String, List<String>>().apply {
put(
"Xiaomi", listOf(
"com.miui.securitycenter/com.miui.permcenter.autostart.AutoStartManagementActivity",
"com.miui.securitycenter"
)
)
put(
"samsung", listOf(
"com.samsung.android.sm_cn/com.samsung.android.sm.ui.ram.AutoRunActivity",
"com.samsung.android.sm_cn/com.samsung.android.sm.ui.appmanagement.AppManagementActivity",
"com.samsung.android.sm_cn/com.samsung.android.sm.ui.cstyleboard.SmartManagerDashBoardActivity",
"com.samsung.android.sm_cn/.ui.ram.RamActivity",
"com.samsung.android.sm_cn/.app.dashboard.SmartManagerDashBoardActivity",
"com.samsung.android.sm/com.samsung.android.sm.ui.ram.AutoRunActivity",
"com.samsung.android.sm/com.samsung.android.sm.ui.appmanagement.AppManagementActivity",
"com.samsung.android.sm/com.samsung.android.sm.ui.cstyleboard.SmartManagerDashBoardActivity",
"com.samsung.android.sm/.ui.ram.RamActivity",
"com.samsung.android.sm/.app.dashboard.SmartManagerDashBoardActivity",
"com.samsung.android.lool/com.samsung.android.sm.ui.battery.BatteryActivity",
"com.samsung.android.sm_cn",
"com.samsung.android.sm"
)
)
put(
"HUAWEI", listOf(
"com.huawei.systemmanager/.startupmgr.ui.StartupNormalAppListActivity",
"com.huawei.systemmanager/.appcontrol.activity.StartupAppControlActivity",
"com.huawei.systemmanager/.optimize.process.ProtectActivity",
"com.huawei.systemmanager/.optimize.bootstart.BootStartActivity",
// "com.huawei.systemmanager/com.huawei.permissionmanager.ui.MainActivity", // 这个是隐私-权限管理,但是没有自启动权限!!!
"com.android.settings/com.android.settings.Settings$" + "AppAndNotificationDashboardActivity", // 鸿蒙系统,应用和服务,列表中有应用启动管理
"com.huawei.systemmanager"
)
)
put(
"vivo", listOf(
"com.iqoo.secure/.ui.phoneoptimize.BgStartUpManager",
"com.vivo.permissionmanager/.activity.BgStartUpManagerActivity",
"com.vivo.permissionmanager/.activity.SoftPermissionDetailActivity",
"com.iqoo.secure/.safeguard.PurviewTabActivity",
"com.iqoo.secure",
"com.vivo.permissionmanager"
)
)
put(
"Meizu", listOf(
"com.meizu.safe/.permission.SmartBGActivity",
"com.meizu.safe/.permission.PermissionMainActivity",
"com.meizu.safe"
)
)
put(
"OPPO", listOf(
"com.coloros.safecenter/.startupapp.StartupAppListActivity",
"com.coloros.safecenter/.permission.startup.StartupAppListActivity",
"com.oppo.safe/.permission.startup.StartupAppListActivity",
"com.coloros.oppoguardelf/com.coloros.powermanager.fuelgaue.PowerUsageModelActivity",
"com.coloros.safecenter/com.coloros.privacypermissionsentry.PermissionTopActivity",
"com.coloros.safecenter",
"com.oppo.safe",
"com.coloros.oppoguardelf"
)
)
put(
"oneplus", listOf(
"com.oneplus.security/.chainlaunch.view.ChainLaunchAppListActivity",
"com.oneplus.security"
)
)
put(
"letv", listOf(
"com.letv.android.letvsafe/.AutobootManageActivity",
"com.letv.android.letvsafe/.BackgroundAppManageActivity",
"com.letv.android.letvsafe"
)
)
put(
"zte", listOf(
"com.zte.heartyservice/.autorun.AppAutoRunManager",
"com.zte.heartyservice"
)
)
//金立
put(
"F", listOf(
"com.gionee.softmanager/.MainActivity",
"com.gionee.softmanager"
)
)
//以下为未确定(厂商名也不确定)
put(
"smartisanos", listOf(
"com.smartisanos.security/.invokeHistory.InvokeHistoryActivity",
"com.smartisanos.security"
)
)
//360
put(
"360", listOf(
"com.yulong.android.coolsafe/.ui.activity.autorun.AutoRunListActivity",
"com.yulong.android.coolsafe"
)
)
//360
put(
"ulong", listOf(
"com.yulong.android.coolsafe/.ui.activity.autorun.AutoRunListActivity",
"com.yulong.android.coolsafe"
)
)
//酷派
put(
"coolpad" /*厂商名称不确定是否正确*/, listOf(
"com.yulong.android.security/com.yulong.android.seccenter.tabbarmain",
"com.yulong.android.security"
)
)
//联想
put(
"lenovo" /*厂商名称不确定是否正确*/, listOf(
"com.lenovo.security/.purebackground.PureBackgroundActivity",
"com.lenovo.security"
)
)
put(
"htc" /*厂商名称不确定是否正确*/, listOf(
"com.htc.pitroad/.landingpage.activity.LandingPageActivity",
"com.htc.pitroad"
)
)
//华硕
put(
"asus" /*厂商名称不确定是否正确*/, listOf(
"com.asus.mobilemanager/.MainActivity",
"com.asus.mobilemanager"
)
)
//酷派
put(
"YuLong", listOf(
"com.yulong.android.softmanager/.SpeedupActivity",
"com.yulong.android.security/com.yulong.android.seccenter.tabbarmain",
"com.yulong.android.security"
)
)
}

fun startAutoBootSetting(context: Context?) {
Log.e("AutoStartHelper", "当前手机型号为:" + Build.MANUFACTURER)
var result = false

run start0@{
for ((manufacturer, componentNameList) in hashMap) {
if (Build.MANUFACTURER.equals(manufacturer, ignoreCase = true)) {
for (actName in componentNameList) {
try {
var intent: Intent? = null
if (actName.contains("/")) {
intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.component = ComponentName.unflattenFromString(actName)
if (actName.contains("SoftPermissionDetailActivity")) {
intent.putExtra("packagename", context?.packageName)
}
}
// else {
// // 跳转到对应的安全管家/安全中心
// intent = context?.packageManager?.getLaunchIntentForPackage(actName)
// }
intent?.let {
context?.startActivity(intent)
result = true
return@start0
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}

if (!result) {
try {
// 跳转到app详情设置
val intent = Intent()
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.data = Uri.fromParts("package", context?.packageName, null)
context?.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
-
作者

Dench

发布于

2022-01-07

更新于

2022-01-07

许可协议

CC BY-NC-SA 4.0

JsBridge 开源库

JsBridge 开源库

项目地址:https://github.com/lzyzsd/JsBridge
使用参考:https://www.jianshu.com/p/7aea03838f19

0x00 从H5界面,跳转Native登录,登录之后重新加载H5页面出现JsBridge注入失败[code=-2,message=net::ERR_NAME_NOT_RESOLVED]

解决方案:
App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js-bridge register error
// 回调 onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
// [code=-2,message=net::ERR_NAME_NOT_RESOLVED]
// so you have to destroy webView and rebuild one.
private void reloadWebView() {
if (mWebView != null) {
mWebView.destroy();
mWebView = null;
}

initView();
initWebView();
webView.loadUrl(url);
}
-
作者

Dench

发布于

2022-06-08

更新于

2022-06-08

许可协议

CC BY-NC-SA 4.0

RecyclerView 根据滑动位置动态改变背景透明度

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
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
onRecyclerScrolled(recyclerView, dx, dy)
}

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
})

private val dp180 = dp2px(180)
private var distanceY = 0
private var current = 0
private fun onRecyclerScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
distanceY += dy
when {
distanceY >= dp180 -> {
if (current == 1) return
recyclerView.setBackgroundColor(Color.argb(255, 246, 248, 250))
current = 1
}
distanceY <= 0 -> {
if (current == 0) return
recyclerView.setBackgroundColor(Color.argb(0, 246, 248, 250))
current = 0
}
else -> {
recyclerView.setBackgroundColor(
Color.argb(
distanceY * 255 / dp180,
246, 248, 250
)
)
current = -1
}
}
}
-
作者

Dench

发布于

2022-06-27

更新于

2022-06-27

许可协议

CC BY-NC-SA 4.0

APK签名之jarsigner签名工具

APK签名之jarsigner签名工具

使用JDK签名工具jarsigner签名APK文件 jarsigner -verbose -keystore [签名文件路径] -signedjar [签名后的apk文件路径] [未签名的apk文件路径] [证书别名]

20230110180748

1
jarsigner -verbose -keystore D:\xxx\xxx.jks -signedjar D:\xxx\xxx_signed.apk D:\xxx\***.apk keyAlias
-
作者

Dench

发布于

2023-01-10

更新于

2023-01-10

许可协议

CC BY-NC-SA 4.0

SystemBarUtil 工具类

SystemBarUtil 工具类

工具类,提供了系统栏高度和屏幕宽高获取方法

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
102
103

import android.app.Activity
import android.content.Context
import android.graphics.Point
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager

/**
* 工具类,提供了系统栏高度和屏幕宽高获取方法
*/
object SystemBarUtil {

/**
* 获取状态栏高度
*/
@JvmStatic
fun getStatusBarHeight(context: Context): Int {
var height = 0
try {
val resourceId = context.applicationContext.resources.getIdentifier(
"status_bar_height",
"dimen",
"android"
)
if (resourceId > 0) {
height =
context.applicationContext.resources.getDimensionPixelSize(resourceId)
}
} catch (e: Exception) {
}
return height
}

/**
* 获取系统导航栏高度
*/
@JvmStatic
fun getNavigationBarHeight(context: Context): Int {
var height = 0
try {
val resourceId = context.applicationContext.resources.getIdentifier(
"navigation_bar_height",
"dimen",
"android"
)
if (resourceId > 0) {
height =
context.applicationContext.resources.getDimensionPixelSize(resourceId)
}
} catch (e: Exception) {
}
return height
}

private const val NAVIGATION = "navigationBarBackground"

// 该方法需要在View完全被绘制出来之后调用
@JvmStatic
private fun isNavigationBarVisible(activity: Activity): Boolean {
val vp = activity.window.decorView as ViewGroup?
if (vp != null) {
for (i in 0 until vp.childCount) {
vp.getChildAt(i).context.packageName
if (vp.getChildAt(i).id !== View.NO_ID &&
NAVIGATION == activity.resources.getResourceEntryName(vp.getChildAt(i).id)
) {
return true
}
}
}
return false
}

/**
* 获取屏幕的物理大小 px
*/
@JvmStatic
fun getDeviceScreenSize(context: Context): Point {
val appContext = context.applicationContext
val wm = appContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val point = Point(0, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
wm.defaultDisplay.getRealSize(point)
} else {
wm.defaultDisplay.getSize(point)
}
return point
}

/**
* 获取显示屏幕的宽高 px
*/
@JvmStatic
fun getDisplaySize(context: Context): Point {
val point = Point(0, 0)
val dm = context.applicationContext.resources.displayMetrics
point.x = dm.widthPixels
point.y = dm.heightPixels
return point
}
}
-
作者

Dench

发布于

2023-02-15

更新于

2023-02-15

许可协议

CC BY-NC-SA 4.0

Android获取文件MD5

Android获取文件MD5

通过 java.security 包下的MessageDigest工具,可以简单快捷的直接计算出文件的MD5值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun getFileMd5(path: String?): String? {
if (path == null || path.isEmpty()) {
return ""
}
try {
val messagedigest = MessageDigest.getInstance("MD5")
val buf = ByteArray(4096)
var n: Int
val fis = FileInputStream(path)
while (fis.read(buf, 0, 4096).also { n = it } > 0) {
messagedigest.update(buf, 0, n)
}
fis.close()
return HexUtils.toHexString(messagedigest.digest())
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
-
1
2
3
4
5
6
7
8
9
10
11
12
13
private static char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(2 * bytes.length);
for (int l = 0; l < bytes.length; l++) {
char c0 = hexDigits[(bytes[l] & 0xf0) >> 4];
char c1 = hexDigits[bytes[l] & 0xf];
sb.append(c0);
sb.append(c1);
}
return sb.toString();
}

作者

Dench

发布于

2023-10-20

更新于

2023-10-20

许可协议

CC BY-NC-SA 4.0

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

0x01 系统环境

Windows 10 专业版 64 位操作系统

+

11th Gen Intel(R) Core(TM) i7-11700 @ 2.50GHz (8 核)

+

Intel(R) UHD Graphics 750

+

0x02 安装 python3 和 git

Download and install
git
and
Python 3.10.6
(tick Add to PATH)

+

0x03 安装 pytorch

https://pytorch.org/get-started/locally/

+

由于当前 PC 的显卡不是英伟达,所以下载的 tarch 版本是 cup 版本

+

检测 pytorch 是否安装成功

+
1
2
python # 打开python环境
import torch
+ +

没有报异常即说明安装成功

+

0x04 安装 stable-diffusion-webui

https://github.com/AUTOMATIC1111/stable-diffusion-webui

+

由于当前 PC 的 CUP 和 GPU 都是 Intel,所以下载 openvino 版本

+
1
2
3
git clone https://github.com/openvinotoolkit/stable-diffusion-webui.git
cd stable-diffusion-webui
.\webui-user.bat # 这里会开始下载,并且安装需要的软件,等待完成
+ +

0x05 下载网络上已经训练好的 AI 模型

现在我们还需要一个训练好的 AI 模型来指导生成效果,在如下网站中可以下载到很多训练好的模型:

+

https://civitai.com/

+

选择一个感兴趣的模型,下载好的,将模型文件放入工程目录下的 models/Stable-diffusion 文件夹下面即可。

+

0x06 本地运行

下面可以尝试运行下此 Web 项目,在工程目录下执行:

+
1
.\webui-user.bat
+ +

执行完成后,在浏览器打开如下地址:

+

http://127.0.0.1:7860/

+

0x07 自定义 OpenVINO 脚本

https://github.com/openvinotoolkit/stable-diffusion-webui/wiki/Custom-Scripts#accelerate-with-openvino

+

选择自定义脚本 Accelerate with OpenVINO,按照文档配置相关参数。之后就可体验 stable-diffusion 生成图片了。

+

Good Luck.

+

参考链接:

https://huishao.cc/2023/07/30/478Mac%E9%83%A8%E7%BD%B2AIGC%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90%E6%9C%8D%E5%8A%A1%E2%80%94%E2%80%94%E5%9F%BA%E4%BA%8Estable-diffusion/

+

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

https://denchopen.github.io/blog/2024/03/07/Windows部署stable-diffusion/

作者

Dench

发布于

2024-03-07

更新于

2024-03-07

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

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

×

\ No newline at end of file diff --git a/404/index.html b/404/index.html index 8f89ee88..240d788a 100644 --- a/404/index.html +++ b/404/index.html @@ -35,7 +35,7 @@ })();
作者

Dench

发布于

1970-01-01

更新于

1970-01-01

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

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

×

\ No newline at end of file diff --git a/archives/2024/index.html b/archives/2024/index.html new file mode 100644 index 00000000..57172a53 --- /dev/null +++ b/archives/2024/index.html @@ -0,0 +1,86 @@ + +归档: 2024 - 个人小站
Your browser is out-of-date!

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

×

\ No newline at end of file diff --git a/archives/index.html b/archives/index.html index 2e59a354..38256f8e 100644 --- a/archives/index.html +++ b/archives/index.html @@ -33,7 +33,7 @@ switchTab(); window.addEventListener('hashchange', switchTab, false); })(); -

Android深色模式适配指南

Android 深色模式(夜间模式)适配指南

Android 10 (API 级别 29) 及更高版本中提供深色主题背景。深色主题背景具有诸多优势:

+

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

0x01 系统环境

Windows 10 专业版 64 位操作系统

+

11th Gen Intel(R) Core(TM) i7-11700 @ 2.50GHz (8 核)

+

Intel(R) UHD Graphics 750

+

0x02 安装 python3 和 git

Download and install
git
and
Python 3.10.6
(tick Add to PATH)

+

0x03 安装 pytorch

https://pytorch.org/get-started/locally/

+

由于当前 PC 的显卡不是英伟达,所以下载的 tarch 版本是 cup 版本

+

检测 pytorch 是否安装成功

+
1
2
python # 打开python环境
import torch
+ +

没有报异常即说明安装成功

+

0x04 安装 stable-diffusion-webui

https://github.com/AUTOMATIC1111/stable-diffusion-webui

+

由于当前 PC 的 CUP 和 GPU 都是 Intel,所以下载 openvino 版本

+
1
2
3
git clone https://github.com/openvinotoolkit/stable-diffusion-webui.git
cd stable-diffusion-webui
.\webui-user.bat # 这里会开始下载,并且安装需要的软件,等待完成
+ +

0x05 下载网络上已经训练好的 AI 模型

现在我们还需要一个训练好的 AI 模型来指导生成效果,在如下网站中可以下载到很多训练好的模型:

+

https://civitai.com/

+

选择一个感兴趣的模型,下载好的,将模型文件放入工程目录下的 models/Stable-diffusion 文件夹下面即可。

+

0x06 本地运行

下面可以尝试运行下此 Web 项目,在工程目录下执行:

+
1
.\webui-user.bat
+ +

执行完成后,在浏览器打开如下地址:

+

http://127.0.0.1:7860/

+

0x07 自定义 OpenVINO 脚本

https://github.com/openvinotoolkit/stable-diffusion-webui/wiki/Custom-Scripts#accelerate-with-openvino

+

选择自定义脚本 Accelerate with OpenVINO,按照文档配置相关参数。之后就可体验 stable-diffusion 生成图片了。

+

Good Luck.

+

参考链接:

https://huishao.cc/2023/07/30/478Mac%E9%83%A8%E7%BD%B2AIGC%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90%E6%9C%8D%E5%8A%A1%E2%80%94%E2%80%94%E5%9F%BA%E4%BA%8Estable-diffusion/

+

Android深色模式适配指南

Android 深色模式(夜间模式)适配指南

Android 10 (API 级别 29) 及更高版本中提供深色主题背景。深色主题背景具有诸多优势:

  • 可大幅减少耗电量(具体取决于设备的屏幕技术)。
  • 为弱视以及对强光敏感的用户提高可视性。
  • @@ -140,17 +166,7 @@

    0x03 使用 am 指令进行测试

    通过如下指令测试调起,如果能够正确地调起页面展示数据则说明 intent-filter 设置成功。

    1
    adb shell am start -W -a "android.intent.action.VIEW" -d "yourUri" yourPackageName
    -

打开手机应用商店的评论调研

打开手机应用商店的评论调研

目前国内主流的应用商店ov,华为,小米和应用宝中,只有oppo和vivo支持APP直接拉起应用商店评分

-

0x01 Oppo应用商店评分

Oppo应用评论调起的官方文档:

-

https://open.oppomobile.com/new/developmentDoc/info?id=11038

-
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
private final static String PKG_MK_HEYTAP = "com.heytap.market";//Q之后的软件商店包名
private final static String PKG_MK_OPPO = "com.oppo.market";//Q之前的软件商店包名
private final static String COMMENT_DEEPLINK_PREFIX = "oaps://mk/developer/comment?pkg=";
private final static int SUPPORT_MK_VERSION = 84000; // 支持评论功能的软件商店版本

/**
* 拉起评论页面。
*/
public static boolean jumpToComment(Activity context) {
// 此处一定要传入调用方自己的包名,不能给其他应用拉起评论页。
String url = COMMENT_DEEPLINK_PREFIX + context.getPackageName();
// 优先判断heytap包
if (getVersionCode(context, PKG_MK_HEYTAP) >= SUPPORT_MK_VERSION) {
return jumpApp(context, Uri.parse(url), PKG_MK_HEYTAP);
}
if (getVersionCode(context, PKG_MK_OPPO) >= SUPPORT_MK_VERSION) {
return jumpApp(context, Uri.parse(url), PKG_MK_OPPO);
}
return false;
}

/**
* 获取目标app版本号~
*
* @param context
* @param packageName
* @return 返回版本号
*/
private static long getVersionCode(Activity context, String packageName) {
long versionCode = -1;
try {
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
if (info != null) {
versionCode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? info.getLongVersionCode() : info.versionCode;
}
} catch (PackageManager.NameNotFoundException e) {
}
return versionCode;
}

private static boolean jumpApp(Activity context, Uri uri, String targetPkgName) {
try {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setPackage(targetPkgName);
intent.setData(uri);
// 建议采用startActivityForResult 方法启动商店页面,requestCode由调用方自定义且必须大于0,软件商店不关注
context.startActivityForResult(intent, 100);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
- -

0x02 Vivo应用商店评分

Vivo应用评论调起的官方文档:

-

https://dev.vivo.com.cn/documentCenter/doc/257

-
1
2
3
4
5
6
String url = market://details?id=${pkg}&th_name=need_comment
Uri uri = Uri.parse(url);
Intent intent= new Intent(Intent.ACTION_VIEW,uri);
intent.setPackage("com.bbk.appstore");
startActivity(intent);

- -

0x03 其他应用商店

直接跳转到应用商店的APP详情页,具体代码如下:

-
1
2
3
4
5
6
7
String url = market://details?id=${pkg}
Uri uri = Uri.parse(url);
Intent intent= new Intent(Intent.ACTION_VIEW,uri);
if (market_pkg != null)
intent.setPackage(market_pkg);
startActivity(intent);

自定义布局:西部世界 第一季

自定义布局:西部世界 第一季

这个自定义布局要求显示为 系列名称... + 第一季 ,后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示。

+

RecyclerView的几种Decoration

RecyclerView的几种Decoration

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
import android.content.res.Resources
import android.graphics.Rect
import android.util.TypedValue
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class SimplePaddingDecoration(
spaceDp: Int,
val orientation: Int = RecyclerView.VERTICAL
) : RecyclerView.ItemDecoration() {
private val dividerHeight: Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
spaceDp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition

if (orientation == RecyclerView.VERTICAL) {
// 竖直
if (position + 1 != parent.adapter?.itemCount) {
outRect.set(0, 0, 0, dividerHeight)
} else {
outRect.set(0, 0, 0, 0)
}
} else {
// 水平
if (position + 1 != parent.adapter?.itemCount) {
outRect.set(0, 0, dividerHeight, 0)
} else {
outRect.set(0, 0, 0, 0)
}
}
}
}
+ +

自定义布局:西部世界 第一季

自定义布局:西部世界 第一季

这个自定义布局要求显示为 系列名称... + 第一季 ,后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示。

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
/**
* 系列名称... + 第一季
* 后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示
*/
class FixedEndLinearLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//获取父布局测量size和model
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
if (childCount != 2) throw RuntimeException("FixedEndLinearLayout must have 2 children.")
val wrapChild = getChildAt(0)
val fixedChild = getChildAt(1)

//测量
measureChild(fixedChild, widthMeasureSpec, heightMeasureSpec)
val fixedParams = fixedChild.layoutParams as MarginLayoutParams

val fixedChildWidth =
fixedChild.measuredWidth + fixedParams.leftMargin + fixedParams.rightMargin
val fixedChildHeight =
fixedChild.measuredHeight + fixedParams.topMargin + fixedParams.bottomMargin

val wrapChildWidthSpec = ViewGroup.getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight + fixedChildWidth, wrapChild.layoutParams.width
)
val wrapChildHeightSpec = ViewGroup.getChildMeasureSpec(
heightMeasureSpec, paddingTop + paddingBottom, wrapChild.layoutParams.height
)
wrapChild.measure(wrapChildWidthSpec, wrapChildHeightSpec)

val wrapParams = wrapChild.layoutParams as MarginLayoutParams

val wrapChildWidth =
wrapChild.measuredWidth + wrapParams.leftMargin + wrapParams.rightMargin
val wrapChildHeight =
wrapChild.measuredHeight + wrapParams.topMargin + wrapParams.bottomMargin

val width = wrapChildWidth + fixedChildWidth
val height = fixedChildHeight.coerceAtLeast(wrapChildHeight)

start0 = paddingLeft + wrapParams.leftMargin
start1 = paddingLeft + wrapChildWidth + fixedParams.leftMargin

setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) widthSize else width + paddingLeft + paddingRight,
if (heightMode == MeasureSpec.EXACTLY) heightSize else height + paddingTop + paddingBottom
)

}

private var start0 = 0
private var start1 = 0

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val wrapChild = getChildAt(0)
val fixedChild = getChildAt(1)
val y0 = (measuredHeight - wrapChild.measuredHeight) / 2
val y1 = (measuredHeight - fixedChild.measuredHeight) / 2
wrapChild.layout(
start0,
y0,
start0 + wrapChild.measuredWidth,
y0 + wrapChild.measuredHeight
)
fixedChild.layout(
start1,
y1,
start1 + fixedChild.measuredWidth,
y1 + fixedChild.measuredHeight
)
}
}

观察者模式Kotlin泛型实现消息中心

观察者模式 + Kotlin 泛型实现的简易版消息中心

-
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
class MessageCenter<T> {
companion object {
private val centers = mutableMapOf<String, Any>()
fun <T> getInstance(clazz: Class<T>): MessageCenter<T> {
return if (centers[clazz.simpleName] != null) {
centers[clazz.simpleName] as MessageCenter<T>
} else {
val messageCenter = MessageCenter<T>()
centers[clazz.simpleName] = messageCenter
messageCenter
}
}
}

fun register(observer: Observer<T>) {
observers.add(observer)
}

fun unregister(observer: Observer<T>) {
if (observers.contains(observer)) {
observers.remove(observer)
}
}

fun post(t: T) {
observers.forEach {
it.receive(t)
}
}

private var observers = mutableListOf<Observer<T>>()
}

interface Observer<T> {
fun receive(t: T)
}

fun main() {
val ob = object : Observer<String> {
override fun receive(t: String) {
println("result is: $t")
}
}
MessageCenter.getInstance(String::class.java).register(ob)
MessageCenter.getInstance(String::class.java).post("txt post.")
MessageCenter.getInstance(String::class.java).unregister(ob)

MessageCenter.getInstance(Int::class.java).post(123)
}
- - - -

BottomFragment

底部弹出控件 - Fragment 实现

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
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.dench.baselib.R
import com.dench.baselib.databinding.FragmentBottomBinding

class BottomFragment : Fragment() {
companion object {
fun start(fm: FragmentManager, fragment: Fragment): BottomFragment {
val bottomFragment = BottomFragment().apply {
setFragment(fragment)
}
fm.beginTransaction()
.setCustomAnimations(
R.anim.fragment_bottom_enter,
0,
0,
R.anim.fragment_bottom_exit
)
.add(android.R.id.content, bottomFragment)
.addToBackStack(null)
.commitAllowingStateLoss()
return bottomFragment
}
}

private lateinit var fragment: Fragment

private fun setFragment(fragment: Fragment) {
this.fragment = fragment
}

private lateinit var binding: FragmentBottomBinding
private val TAG = "BottomFragment"

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentBottomBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.run {
binding.bottomFragmentRl.setOnClickListener(View.OnClickListener {
Log.d(TAG, "bottom root view click.")
dismissSelf()
})

/** add fragment */
childFragmentManager.beginTransaction()
.add(R.id.container, fragment)
.commitAllowingStateLoss()
}
}

private fun dismissSelf() {
parentFragmentManager.popBackStack()
}
}
+

观察者模式Kotlin泛型实现消息中心

观察者模式 + Kotlin 泛型实现的简易版消息中心

+
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
class MessageCenter<T> {
companion object {
private val centers = mutableMapOf<String, Any>()
fun <T> getInstance(clazz: Class<T>): MessageCenter<T> {
return if (centers[clazz.simpleName] != null) {
centers[clazz.simpleName] as MessageCenter<T>
} else {
val messageCenter = MessageCenter<T>()
centers[clazz.simpleName] = messageCenter
messageCenter
}
}
}

fun register(observer: Observer<T>) {
observers.add(observer)
}

fun unregister(observer: Observer<T>) {
if (observers.contains(observer)) {
observers.remove(observer)
}
}

fun post(t: T) {
observers.forEach {
it.receive(t)
}
}

private var observers = mutableListOf<Observer<T>>()
}

interface Observer<T> {
fun receive(t: T)
}

fun main() {
val ob = object : Observer<String> {
override fun receive(t: String) {
println("result is: $t")
}
}
MessageCenter.getInstance(String::class.java).register(ob)
MessageCenter.getInstance(String::class.java).post("txt post.")
MessageCenter.getInstance(String::class.java).unregister(ob)

MessageCenter.getInstance(Int::class.java).post(123)
}
+ + + +

BottomFragment

底部弹出控件 - Fragment 实现

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
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.dench.baselib.R
import com.dench.baselib.databinding.FragmentBottomBinding

class BottomFragment : Fragment() {
companion object {
fun start(fm: FragmentManager, fragment: Fragment): BottomFragment {
val bottomFragment = BottomFragment().apply {
setFragment(fragment)
}
fm.beginTransaction()
.setCustomAnimations(
R.anim.fragment_bottom_enter,
0,
0,
R.anim.fragment_bottom_exit
)
.add(android.R.id.content, bottomFragment)
.addToBackStack(null)
.commitAllowingStateLoss()
return bottomFragment
}
}

private lateinit var fragment: Fragment

private fun setFragment(fragment: Fragment) {
this.fragment = fragment
}

private lateinit var binding: FragmentBottomBinding
private val TAG = "BottomFragment"

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentBottomBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.run {
binding.bottomFragmentRl.setOnClickListener(View.OnClickListener {
Log.d(TAG, "bottom root view click.")
dismissSelf()
})

/** add fragment */
childFragmentManager.beginTransaction()
.add(R.id.container, fragment)
.commitAllowingStateLoss()
}
}

private fun dismissSelf() {
parentFragmentManager.popBackStack()
}
}

为什么选择Netty

为什么选择Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他如Strom还有业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

通过对Netty的分析,我们将它的优点总结如下。

@@ -262,17 +267,7 @@

0x04 遇到的坑

  • 等标签,如果使用databinding ,子布局xml的 root tag 依旧需要layout 标签嵌套 data 标签。否者编译报错,找不到对应的属性
-

自定义Notification遇到的坑

自定义Notification 实现:

-
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
// RemoteViews for notification
private var rv: RemoteViews? = null
private var rvExpanded: RemoteViews? = null
private fun customNotification(process: Int) {
// custom RemoteViews
if (rv == null) rv = RemoteViews(packageName, R.layout.notification_small)
rv?.setTextViewText(R.id.notification_title, "这是一个小标题")
if (rvExpanded == null) rvExpanded = RemoteViews(packageName, R.layout.notification_large)
rvExpanded?.setTextViewText(R.id.large_notification_title, "这是一个大标题,支持很多的内容: $process%")

// PendingIntent
val intentNotification = Intent(this, PlayMusicActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendIntent = PendingIntent.getActivity(
this,
0,
intentNotification,
PendingIntent.FLAG_UPDATE_CURRENT
)

// build notification
val customNotification =
NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.bravo) // small Icon
.setStyle(NotificationCompat.DecoratedCustomViewStyle()) // 自定义contentView
.setCustomContentView(rv!!)
.setContent(rvExpanded!!)
.setCustomBigContentView(rvExpanded!!)
.setCustomHeadsUpContentView(rvExpanded!!)
.setOngoing(true) // 一直显示
.setAutoCancel(false) // 点击后消失
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏显示,需要配合权限设置
.setPriority(NotificationCompat.PRIORITY_HIGH) // Priority
.setOnlyAlertOnce(true) // 声音,震动,仅弹出一次
.setContentIntent(pendIntent)
.build()
startForeground(NOTIFICATION_ID, customNotification)
}
- - - -

1.使用 NotificationCompat 兼容各个版本差异性

-

2.RemoteViews 布局文件不支持 constraintlayout ,切记

-

3.在SDK 26之后必须要绑定Channel,所以通知要先创建Channel

-
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
fun checkAndCreateChannel(
context: Context,
channelId: String,
channelName: String,
desc: String = channelName
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
try {
// 查找分组
val nc = notificationManager.getNotificationChannel(channelId)
Log.d("ChannelHelper", "${nc.id} Notification Channel exist.")
} catch (e: Exception) {
Log.d("ChannelHelper", "empty channel need create.")
// 创建分组
val mChannel =
NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = desc
enableLights(true)
enableVibration(true)
}
notificationManager.createNotificationChannel(mChannel)
}
}
}
- -

Android-JsBridge实现本地H5混合开发

Android JsBridge 混合开发框架

0x01 Java 调用 Js

我们知道,native层调用h5,在WebView中,如果java要调用js的方法,

+

自定义Notification遇到的坑

自定义Notification 实现:

+
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
// RemoteViews for notification
private var rv: RemoteViews? = null
private var rvExpanded: RemoteViews? = null
private fun customNotification(process: Int) {
// custom RemoteViews
if (rv == null) rv = RemoteViews(packageName, R.layout.notification_small)
rv?.setTextViewText(R.id.notification_title, "这是一个小标题")
if (rvExpanded == null) rvExpanded = RemoteViews(packageName, R.layout.notification_large)
rvExpanded?.setTextViewText(R.id.large_notification_title, "这是一个大标题,支持很多的内容: $process%")

// PendingIntent
val intentNotification = Intent(this, PlayMusicActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendIntent = PendingIntent.getActivity(
this,
0,
intentNotification,
PendingIntent.FLAG_UPDATE_CURRENT
)

// build notification
val customNotification =
NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.bravo) // small Icon
.setStyle(NotificationCompat.DecoratedCustomViewStyle()) // 自定义contentView
.setCustomContentView(rv!!)
.setContent(rvExpanded!!)
.setCustomBigContentView(rvExpanded!!)
.setCustomHeadsUpContentView(rvExpanded!!)
.setOngoing(true) // 一直显示
.setAutoCancel(false) // 点击后消失
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏显示,需要配合权限设置
.setPriority(NotificationCompat.PRIORITY_HIGH) // Priority
.setOnlyAlertOnce(true) // 声音,震动,仅弹出一次
.setContentIntent(pendIntent)
.build()
startForeground(NOTIFICATION_ID, customNotification)
}
+ + + +

1.使用 NotificationCompat 兼容各个版本差异性

+

2.RemoteViews 布局文件不支持 constraintlayout ,切记

+

3.在SDK 26之后必须要绑定Channel,所以通知要先创建Channel

+
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
fun checkAndCreateChannel(
context: Context,
channelId: String,
channelName: String,
desc: String = channelName
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
try {
// 查找分组
val nc = notificationManager.getNotificationChannel(channelId)
Log.d("ChannelHelper", "${nc.id} Notification Channel exist.")
} catch (e: Exception) {
Log.d("ChannelHelper", "empty channel need create.")
// 创建分组
val mChannel =
NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = desc
enableLights(true)
enableVibration(true)
}
notificationManager.createNotificationChannel(mChannel)
}
}
}
+ +

Android-JsBridge实现本地H5混合开发

Android JsBridge 混合开发框架

0x01 Java 调用 Js

我们知道,native层调用h5,在WebView中,如果java要调用js的方法,

0x0101 loadUrl <4.4

Android4.4以前使用WebView.loadUrl("javascript:function()")

0x0102 evaluateJavascript >4.4

Android4.4以后,使用以下方式

1
2
3
4
5
6
webView.evaluateJavascript("javascript:function()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Toast.makeText(MainActivity.this, "onReceiveValue From JS: " + value, Toast.LENGTH_SHORT).show();
}
});
@@ -72,7 +82,7 @@

StatusBarHelper

StatusBarHelper

StatusBar 工具类

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
object StatusBarHelper {
/**
* 内容显示在状态栏下面(LAYOUT_FULLSCREEN) > 6.0
* true:白底黑字,false:黑底白字
*/
fun fitSystemBar(activity: Activity, light: Boolean = true) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
val window = activity.window
val decorView = window.decorView
var visibility = decorView.systemUiVisibility
visibility = visibility or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
visibility = if (light) {
visibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
visibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
decorView.systemUiVisibility = visibility

window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.statusBarColor = Color.TRANSPARENT
}

/**
* 调整状态栏文字、图标颜色 > 6.0
* true:白底黑字,false:黑底白字
*/
fun lightStatusBar(activity: Activity, light: Boolean = true) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
var window: Window = activity.window
var visibility = window.decorView.systemUiVisibility
visibility = if (light) {
visibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
visibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
window.decorView.systemUiVisibility = visibility
}


// 获取状态栏高度
fun getStatusBarHeight(activity: Activity): Int {
var result: Int = 0
var resId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resId > 0) result = activity.resources.getDimensionPixelOffset(resId)
return result
}
}
阅读更多

多线程编程

多线程编程

0x01 生产者和消费者问题

1.BlockingQueue 阻塞队列实现

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
// 生产者
class Producer implements Runnable {
private final BlockingQueue<Integer> queue;

public Producer(BlockingQueue queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
try {
queue.put(produce());
System.out.println(Thread.currentThread().getName() + " put a message, total = " + queue.size());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private int produce() {
int x = (int) (Math.random() * 10);
System.out.println(Thread.currentThread().getName() + " put: " + x);
return x;
}
}
阅读更多

链表常见问题

链表常见问题

1
2
3
4
5
6
7
8
9
10
/**
* 单链表数据结构
*/
class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
}
+

多线程编程

多线程编程

0x01 生产者和消费者问题

1.BlockingQueue 阻塞队列实现

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
// 生产者
class Producer implements Runnable {
private final BlockingQueue<Integer> queue;

public Producer(BlockingQueue queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
try {
queue.put(produce());
System.out.println(Thread.currentThread().getName() + " put a message, total = " + queue.size());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private int produce() {
int x = (int) (Math.random() * 10);
System.out.println(Thread.currentThread().getName() + " put: " + x);
return x;
}
}
阅读更多

链表常见问题

链表常见问题

1
2
3
4
5
6
7
8
9
10
/**
* 单链表数据结构
*/
class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
}

0x01 链表翻转

题目描述:

这道算法题,说直白点就是:如何让后一个节点指向前一个节点。

@@ -86,12 +86,7 @@

有一个文本串S,和一个模式串P,现在要查找P在S中的位置

0x01 暴力算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 暴力查找法:不一致的时候直接把i 移动到 i+1 的位置继续比较
private static int search(String s, String p) {
int sLen = s.length();
int pLen = p.length();
int i = 0, j = 0;
while (i < sLen && j < pLen) {
if (s.charAt(i) == p.charAt(j)) {
// 字符相同,则继续匹配下一个字符
i++;
j++;
} else {
// i,j 复位
i = i - j + 1;
j = 0;
}
}
if (j == pLen) return i - j;
return -1;
}

阅读更多

十大排序算法实现(Java版)

十大排序算法实现(Java版)

一. 常用排序算法

1
2
3
4
5
// Java 的进制数表示
int a10 = 99;
int a2 = 0b101;
int a8 = 0143;
int a16 = 0x63;
-

0x01 冒泡排序

原理
比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

阅读更多

设计模式@Java

设计模式 Java版

设计模式(Design Pattern)的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。正确使用设计模式具有以下优点:

-
    -
  • 可以提高程序员的思维能力、编程能力和设计能力。
  • -
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • -
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
  • -
阅读更多

Vim 编程实践

1. 基础设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"显示行
set nu

" 设置屏幕滚动时在光标上下方保留5行预览代码
set so=5

" 设置debug为 zd
nnoremap zd :action Debug<CR>

" 设置run 为 zr
nnoremap zr :action Run<CR>

" 插入模式下 jk 映射 Esc
inoremap jk <Esc>
阅读更多

Gradle常用命令

Gradle 常用命令

0x01 快速构建命令

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
# 查看构建版本
./gradlew -v

# 清除build文件夹
./gradlew clean

# 检查依赖并编译打包
./gradlew build

# 编译并安装debug包
./gradlew installDebug

# 编译并打印日志
./gradlew build --info

# 译并输出性能报告,性能报告一般在 构建工程根目录 build/reports/profile
./gradlew build --profile

# 调试模式构建并打印堆栈日志
./gradlew build --info --debug --stacktrace

# 离线模式
./gradlew aDR --offline

# 守护进程
./gradlew build --daemon

# 并行编译模式
./gradlew build --parallel --parallel-threads=N
+

设计模式@Java

设计模式 Java版

设计模式(Design Pattern)的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。正确使用设计模式具有以下优点:

+
    +
  • 可以提高程序员的思维能力、编程能力和设计能力。
  • +
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • +
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
  • +
阅读更多

Vim 编程实践

1. 基础设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"显示行
set nu

" 设置屏幕滚动时在光标上下方保留5行预览代码
set so=5

" 设置debug为 zd
nnoremap zd :action Debug<CR>

" 设置run 为 zr
nnoremap zr :action Run<CR>

" 插入模式下 jk 映射 Esc
inoremap jk <Esc>
阅读更多

Gradle常用命令

Gradle 常用命令

0x01 快速构建命令

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
# 查看构建版本
./gradlew -v

# 清除build文件夹
./gradlew clean

# 检查依赖并编译打包
./gradlew build

# 编译并安装debug包
./gradlew installDebug

# 编译并打印日志
./gradlew build --info

# 译并输出性能报告,性能报告一般在 构建工程根目录 build/reports/profile
./gradlew build --profile

# 调试模式构建并打印堆栈日志
./gradlew build --info --debug --stacktrace

# 离线模式
./gradlew aDR --offline

# 守护进程
./gradlew build --daemon

# 并行编译模式
./gradlew build --parallel --parallel-threads=N

0x02 构建并安装命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 编译并打Debug包
./gradlew assembleDebug

# 这个是简写 assembleDebug
./gradlew aD

# 编译并打Release的包
./gradlew assembleRelease

# 这个是简写 assembleRelease
./gradlew aR

# Debug模式打包并安装
./gradlew install app:assembleDebug

# Release模式打包并安装
./gradlew installRelease

# 卸载Release模式包
./gradlew uninstallRelease

# Flavor渠道包
./gradlew install app:assemble<Flavor>Debug

@@ -68,9 +73,7 @@

阅读更多

ZSH & iTerm2 的最佳实现

ZSH & iTerm2 的最佳实现

+

Git手册

Git Hand Book

Git 官方站点: http://git-scm.com/download

-

0x01 全局信息设置

1
2
3
4
5
6
$ git config --global http.sslVerify false # 取消ssl认证
## 用户名和邮箱
$ git config --global user.name "git"
$ git config --global user.email "git@gmail.com"
## 查看当前配置
$ git config --list # 查看
- -

0x02 创建 Git 仓库

1 从远程仓库克隆到本地

1
2
3
$ git clone git://github.com/schacon/grit.git
$ git clone git://github.com/schacon/grit.git mygrit # local path
$ git clone git://github.com/schacon/grit.git mygrit --recursive # 递归拉取子module
- -

2 Create a new repository

1
2
3
4
5
6
$ echo "Git Hand Book" >> README.md
$ git init
$ git add README.md
$ git commit -m "add README"
$ git remote add origin git://github.com/schacon/grit.git
$ git push -u origin master
- -

3 Existing folder

1
2
3
4
5
6
$ cd existing_folder
$ git init
$ git remote add origin git://github.com/schacon/grit.git
$ git add .
$ git commit
$ git push -u origin master
- -

4 Existing Git repository

1
2
3
4
$ cd existing_repo
$ git remote add origin git://github.com/schacon/grit.git
$ git push -u origin --all
$ git push -u origin --tags
阅读更多

Python爬取网络图片

+

Git手册

Git Hand Book

Git 官方站点: http://git-scm.com/download

+

0x01 全局信息设置

1
2
3
4
5
6
$ git config --global http.sslVerify false # 取消ssl认证
## 用户名和邮箱
$ git config --global user.name "git"
$ git config --global user.email "git@gmail.com"
## 查看当前配置
$ git config --list # 查看
+ +

0x02 创建 Git 仓库

1 从远程仓库克隆到本地

1
2
3
$ git clone git://github.com/schacon/grit.git
$ git clone git://github.com/schacon/grit.git mygrit # local path
$ git clone git://github.com/schacon/grit.git mygrit --recursive # 递归拉取子module
+ +

2 Create a new repository

1
2
3
4
5
6
$ echo "Git Hand Book" >> README.md
$ git init
$ git add README.md
$ git commit -m "add README"
$ git remote add origin git://github.com/schacon/grit.git
$ git push -u origin master
+ +

3 Existing folder

1
2
3
4
5
6
$ cd existing_folder
$ git init
$ git remote add origin git://github.com/schacon/grit.git
$ git add .
$ git commit
$ git push -u origin master
+ +

4 Existing Git repository

1
2
3
4
$ cd existing_repo
$ git remote add origin git://github.com/schacon/grit.git
$ git push -u origin --all
$ git push -u origin --tags
阅读更多

Python爬取网络图片

直接上源码

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
'''
用Python爬某新闻网站的照片
PS:仅测试爬虫功能,滥用后果自负
'''
import requests
from hashlib import md5
import re
import os

WEB_URL = 'https://new.qq.com/omn/20200207/20200207A0OSQX00.html'
IMAGE_PATH='/Users/**/workspaces/pythonP/img1'

# 通过正则找出网页中所有的图片地址
def find_img_url():
r = requests.get(WEB_URL)
r.raise_for_status()
r.encoding = r.apparent_encoding
demo = r.text
list = []
# ".*?" :正则表达式匹配任意字符串
pattern1 = '<img src=".*?" class="content-picture">'
results1 = re.findall(pattern1, demo)
for res1 in results1:
# <img src="//inews.gtimg.com/newsapp_bt/0/11299639206/1000" class="content-picture">
# https://inews.gtimg.com/newsapp_bt/0/11299639205/1000
res1 = res1.replace('<img src=\"','https:')
res1 = res1.replace('\" class=\"content-picture\">','')
list.append(res1)
# print(res1)
pattern2 = '\"http://inews.gtimg.com/newsapp_bt/0/.*?/1000\"'
results2 = re.findall(pattern2, demo)
for res2 in results2:
res2 = res2.replace('\"','')
list.append(res2)
# print(res2)
return list

# 下载图片到本地
def download_image(img_url):
r = requests.get(img_url)
r.raise_for_status()
content = r.content
file_path = '{0}/{1}.{2}'.format(IMAGE_PATH, md5(content).hexdigest(), 'jpg')
# print(file_path)
if not os.path.exists(file_path):#os.path.exists(file_path)判断文件是否存在,存在返回1,不存在返回0
with open(file_path, 'wb') as f:
f.write(content)
f.close()

list = find_img_url()
for i in list:
print(i, ' download...')
download_image(i)
print("一共下载{}条数据!".format(len(list)))

Hexo & NexT 搭建个人网站

1. 搭建环境(2020-2-6)

    @@ -78,7 +87,7 @@

    1. 检视阅读(泛读)

    1.1 有系统的略读

    • 时间控制在5-15分钟
    • -
    • 先看书名
阅读更多

TextView文字颜色渐变

TextView文字颜色渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import android.graphics.LinearGradient
import android.graphics.Shader
import android.widget.TextView

/**
* 左到右渐变
*/
fun TextView.setHorizontalGradientTextColor(startColor: Int, endColor: Int) {
val x1 = this.paint.measureText(this.text.toString())//测量文本 宽度
val shader = LinearGradient(0f, 0f, x1, 0f, startColor, endColor, Shader.TileMode.CLAMP)
this.paint.shader = shader
this.invalidate()
}

/**
* 上到下渐变
*/
fun TextView.setVerticalGradientTextColor(startColor: Int, endColor: Int) {
val y1 = this.paint.textSize//测量文本 高度
val shader = LinearGradient(0f, 0f, 0f, y1, startColor, endColor, Shader.TileMode.CLAMP)
this.paint.shader = shader
this.invalidate()
}
+

打开手机应用商店的评论调研

打开手机应用商店的评论调研

目前国内主流的应用商店ov,华为,小米和应用宝中,只有oppo和vivo支持APP直接拉起应用商店评分

+

0x01 Oppo应用商店评分

Oppo应用评论调起的官方文档:

+

https://open.oppomobile.com/new/developmentDoc/info?id=11038

+
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
private final static String PKG_MK_HEYTAP = "com.heytap.market";//Q之后的软件商店包名
private final static String PKG_MK_OPPO = "com.oppo.market";//Q之前的软件商店包名
private final static String COMMENT_DEEPLINK_PREFIX = "oaps://mk/developer/comment?pkg=";
private final static int SUPPORT_MK_VERSION = 84000; // 支持评论功能的软件商店版本

/**
* 拉起评论页面。
*/
public static boolean jumpToComment(Activity context) {
// 此处一定要传入调用方自己的包名,不能给其他应用拉起评论页。
String url = COMMENT_DEEPLINK_PREFIX + context.getPackageName();
// 优先判断heytap包
if (getVersionCode(context, PKG_MK_HEYTAP) >= SUPPORT_MK_VERSION) {
return jumpApp(context, Uri.parse(url), PKG_MK_HEYTAP);
}
if (getVersionCode(context, PKG_MK_OPPO) >= SUPPORT_MK_VERSION) {
return jumpApp(context, Uri.parse(url), PKG_MK_OPPO);
}
return false;
}

/**
* 获取目标app版本号~
*
* @param context
* @param packageName
* @return 返回版本号
*/
private static long getVersionCode(Activity context, String packageName) {
long versionCode = -1;
try {
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA);
if (info != null) {
versionCode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? info.getLongVersionCode() : info.versionCode;
}
} catch (PackageManager.NameNotFoundException e) {
}
return versionCode;
}

private static boolean jumpApp(Activity context, Uri uri, String targetPkgName) {
try {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setPackage(targetPkgName);
intent.setData(uri);
// 建议采用startActivityForResult 方法启动商店页面,requestCode由调用方自定义且必须大于0,软件商店不关注
context.startActivityForResult(intent, 100);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
+ +

0x02 Vivo应用商店评分

Vivo应用评论调起的官方文档:

+

https://dev.vivo.com.cn/documentCenter/doc/257

+
1
2
3
4
5
6
String url = market://details?id=${pkg}&th_name=need_comment
Uri uri = Uri.parse(url);
Intent intent= new Intent(Intent.ACTION_VIEW,uri);
intent.setPackage("com.bbk.appstore");
startActivity(intent);

+ +

0x03 其他应用商店

直接跳转到应用商店的APP详情页,具体代码如下:

+
1
2
3
4
5
6
7
String url = market://details?id=${pkg}
Uri uri = Uri.parse(url);
Intent intent= new Intent(Intent.ACTION_VIEW,uri);
if (market_pkg != null)
intent.setPackage(market_pkg);
startActivity(intent);

TextView文字颜色渐变

TextView文字颜色渐变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import android.graphics.LinearGradient
import android.graphics.Shader
import android.widget.TextView

/**
* 左到右渐变
*/
fun TextView.setHorizontalGradientTextColor(startColor: Int, endColor: Int) {
val x1 = this.paint.measureText(this.text.toString())//测量文本 宽度
val shader = LinearGradient(0f, 0f, x1, 0f, startColor, endColor, Shader.TileMode.CLAMP)
this.paint.shader = shader
this.invalidate()
}

/**
* 上到下渐变
*/
fun TextView.setVerticalGradientTextColor(startColor: Int, endColor: Int) {
val y1 = this.paint.textSize//测量文本 高度
val shader = LinearGradient(0f, 0f, 0f, y1, startColor, endColor, Shader.TileMode.CLAMP)
this.paint.shader = shader
this.invalidate()
}

使用方式,调用扩展方法即可

textView.setHorizontalGradientTextColor(Color.RED, Color.GREEN)

@@ -168,15 +178,7 @@

解决方案二:

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

-

RecyclerView+SnapHelper实现ViewPager滑动效果

RecyclerView+SnapHelper实现ViewPager滑动效果

SnapHelper结合RecyclerView使用,能很方便的实现ViewPager滑动效果。SnapHelper是一个抽象类,Google内置了两个默认实现类,LinearSnapHelper和PagerSnapHelper。

-

LinearSnapHelper的使用方法

使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动多个条目。

-
1
2
3
4
5
LinearLayoutManager manager = new LinearLayoutManager(getContext());
manager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(manager);
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);
- -

PagerSnapHelper的使用方法

使RecyclerView像ViewPager一样的效果,每次只能滑动一页。

-
1
2
3
4
5
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);
- -

原文地址:https://developer.aliyun.com/article/665537

-

Aria下载器源码分析

Aria下载器源码分析

Aria 中文文档: https://aria.laoyuyu.me/aria_doc/

+

RecyclerView+SnapHelper实现ViewPager滑动效果

RecyclerView+SnapHelper实现ViewPager滑动效果

SnapHelper结合RecyclerView使用,能很方便的实现ViewPager滑动效果。SnapHelper是一个抽象类,Google内置了两个默认实现类,LinearSnapHelper和PagerSnapHelper。

+

LinearSnapHelper的使用方法

使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动多个条目。

+
1
2
3
4
5
LinearLayoutManager manager = new LinearLayoutManager(getContext());
manager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(manager);
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);
+ +

PagerSnapHelper的使用方法

使RecyclerView像ViewPager一样的效果,每次只能滑动一页。

+
1
2
3
4
5
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);
+ +

原文地址:https://developer.aliyun.com/article/665537

+

Aria下载器源码分析

Aria下载器源码分析

Aria 中文文档: https://aria.laoyuyu.me/aria_doc/

版本:3.8.15

0x01 注册流程

在Activity的onCreate、fragment的onCreate、java的构造函数中使用Aria.download(this).register()便可以实现注册。

0x0101 Aria类,下载库的统一入口

Aria类仅一个私有的构造方法,无法实例化

@@ -108,10 +116,7 @@

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.e(TAG, "The default network is now: " + network)
}

override fun onLost(network: Network) {
Log.e(TAG, "The application no longer has a default network. The last default network was " + network)
handle(null)
}

override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
Log.d(TAG, "The default network changed capabilities: " + networkCapabilities)
handle(networkCapabilities)
}

override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
Log.i(TAG, "The default network changed link properties: " + linkProperties)
}
})

解析NetworkCapabilities的网络状态信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 private fun handle(caps: NetworkCapabilities?) {
if (caps != null) {
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
if (
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
) {
setResult(STATE_WIFI)
return
} else if (
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
) {
setResult(STATE_MOBILE)
return
}
}
}
setResult(STATE_UNKNOWN)
return
}
-

APK签名之jarsigner签名工具

APK签名之jarsigner签名工具

使用JDK签名工具jarsigner签名APK文件 jarsigner -verbose -keystore [签名文件路径] -signedjar [签名后的apk文件路径] [未签名的apk文件路径] [证书别名]

-

20230110180748

-
1
jarsigner -verbose -keystore D:\xxx\xxx.jks -signedjar D:\xxx\xxx_signed.apk D:\xxx\***.apk keyAlias
-

Android首页灰色实现方案

Activity设置灰色

使用ColorMatrix设置灰度

+

APK签名之jarsigner签名工具

APK签名之jarsigner签名工具

使用JDK签名工具jarsigner签名APK文件 jarsigner -verbose -keystore [签名文件路径] -signedjar [签名后的apk文件路径] [未签名的apk文件路径] [证书别名]

+

20230110180748

+
1
jarsigner -verbose -keystore D:\xxx\xxx.jks -signedjar D:\xxx\xxx_signed.apk D:\xxx\***.apk keyAlias
+

Android首页灰色实现方案

Activity设置灰色

使用ColorMatrix设置灰度

1
2
3
4
5
6
7
private fun setGrayPaint(view: View) {
val paint = Paint()
val cm = ColorMatrix()
cm.setSaturation(0f)
paint.colorFilter = ColorMatrixColorFilter(cm)
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint)
}

给首页Activity的decorView设置灰度Paint

@@ -167,15 +170,7 @@

0x06 问题

0x0601 如果Shift + F6重命名时没有Rename Package选项

如果Shift + F6重命名时没有Rename Package选项, 并且出现了以下提示,
Package 'example' contains directories in libraries which cannot be renamed. Do you want to rename current directory or all directories in project?
这是因为依赖包中也存在example这个包名,导致无法直接重命名包名。解决的方案分两种:

方案1. 暂时先移除包含example包名的依赖包,等重命名之后重新添加到项目中
方案2. 只能新建package,然后将要rename的包拖动到新的package中,或者F6移动

PS:因为无法重命名,刚刚经历了手动移动十几个module的package包路径的痛苦历程。

-

RoundImageView圆角控件

RoundImageView圆角控件

示例代码如下:

-
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 android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;

public class RoundImageView extends AppCompatImageView {
private static final String TAG = "RoundImageView";

private int radius = 0;

public RoundImageView(Context context) {
this(context, null);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup(context, attrs, defStyleAttr);
}

private void setup(Context context, AttributeSet attrs, int defStyleAttr) {
try {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
radius = a.getDimensionPixelSize(R.styleable.RoundImageView_riv_radius, 0);
Log.d(TAG, "RoundImageView: radius=" + radius);
a.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}

public void setRadius(int radius) {
this.radius = radius;
}

@Override
protected void onDraw(Canvas canvas) {
if (radius > 0) {
Path path = new Path();
path.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), radius, radius, Path.Direction.CW);
canvas.clipPath(path);//设置可显示的区域,canvas四个角会被剪裁掉
}
super.onDraw(canvas);
}
}

- -

attrs.xml 文件中定义控件的圆角dp值属性:

-
1
2
3
<declare-styleable name="RoundImageView">
<attr name="riv_radius" format="dimension" />
</declare-styleable>
- -

使用示例

-
1
2
3
4
5
6
<com.xx.ui.widget.RoundImageView
android:id="@+id/image_view"
android:layout_width="120dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
app:riv_radius="8dp" />
-

Gradle的环境配置

Gradle的环境配置

原文地址:https://www.cnblogs.com/baiqiantao/p/6890674.html

+

RoundImageView圆角控件

RoundImageView圆角控件

示例代码如下:

+
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 android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;

public class RoundImageView extends AppCompatImageView {
private static final String TAG = "RoundImageView";

private int radius = 0;

public RoundImageView(Context context) {
this(context, null);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup(context, attrs, defStyleAttr);
}

private void setup(Context context, AttributeSet attrs, int defStyleAttr) {
try {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
radius = a.getDimensionPixelSize(R.styleable.RoundImageView_riv_radius, 0);
Log.d(TAG, "RoundImageView: radius=" + radius);
a.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}

public void setRadius(int radius) {
this.radius = radius;
}

@Override
protected void onDraw(Canvas canvas) {
if (radius > 0) {
Path path = new Path();
path.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), radius, radius, Path.Direction.CW);
canvas.clipPath(path);//设置可显示的区域,canvas四个角会被剪裁掉
}
super.onDraw(canvas);
}
}

+ +

attrs.xml 文件中定义控件的圆角dp值属性:

+
1
2
3
<declare-styleable name="RoundImageView">
<attr name="riv_radius" format="dimension" />
</declare-styleable>
+ +

使用示例

+
1
2
3
4
5
6
<com.xx.ui.widget.RoundImageView
android:id="@+id/image_view"
android:layout_width="120dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
app:riv_radius="8dp" />
+

JsBridge 开源库

JsBridge 开源库

项目地址:https://github.com/lzyzsd/JsBridge
使用参考:https://www.jianshu.com/p/7aea03838f19

-

0x00 从H5界面,跳转Native登录,登录之后重新加载H5页面出现JsBridge注入失败[code=-2,message=net::ERR_NAME_NOT_RESOLVED]

解决方案:
App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js-bridge register error
// 回调 onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
// [code=-2,message=net::ERR_NAME_NOT_RESOLVED]
// so you have to destroy webView and rebuild one.
private void reloadWebView() {
if (mWebView != null) {
mWebView.destroy();
mWebView = null;
}

initView();
initWebView();
webView.loadUrl(url);
}
-

打开手机自带的应用商店

需求背景:
因为各种原因,需要打开手机自带的应用商店

+

JsBridge 开源库

JsBridge 开源库

项目地址:https://github.com/lzyzsd/JsBridge
使用参考:https://www.jianshu.com/p/7aea03838f19

+

0x00 从H5界面,跳转Native登录,登录之后重新加载H5页面出现JsBridge注入失败[code=-2,message=net::ERR_NAME_NOT_RESOLVED]

解决方案:
App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js-bridge register error
// 回调 onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
// [code=-2,message=net::ERR_NAME_NOT_RESOLVED]
// so you have to destroy webView and rebuild one.
private void reloadWebView() {
if (mWebView != null) {
mWebView.destroy();
mWebView = null;
}

initView();
initWebView();
webView.loadUrl(url);
}
+

打开手机自带的应用商店

需求背景:
因为各种原因,需要打开手机自带的应用商店

核心代码

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

import android.content.Context
import android.content.Intent
import android.net.Uri

object MarketUtils {
const val XIAOMI_MARKET = "com.xiaomi.market"
const val HUAWEI_MARKET = "com.huawei.appmarket"
const val OPPO_MARKET = "com.oppo.market"
const val OPPO_MARKET2 = "com.heytap.market"
const val VIVO_MARKET = "com.bbk.appstore"
const val MEIZU_MARKET = "com.meizu.mstore"
const val YYB_MARKET = "com.tencent.android.qqdownloader"

fun startTargetMarket(context: Context, deepLink: String, packageName: String) {
//"deeplink": "market://details?id=com.taobao.taobao&..."
try {
val uri = Uri.parse(deepLink)
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.setPackage(packageName)
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
val uri = Uri.parse(deepLink)
val intent = Intent(Intent.ACTION_VIEW, uri)
context.startActivity(intent)
}
}

/**
* 跳转到腾讯应用宝
*/
private fun startTencentMarket(context: Context, deepLink: String) {
val uri: Uri = Uri.parse(deepLink) // "market://details?id=com.taobao.taobao&..."
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.setClassName(
"com.tencent.android.qqdownloader",
"com.tencent.pangu.link.LinkProxyActivity"
)
context.startActivity(intent)
}
}
@@ -63,29 +66,7 @@
1
2
3
4
5
6
7
8
9
10
11
val spanStr = SpannableStringBuilder()
spanStr.append("# ")
spanStr.append(title)
val imageSpan = CenteredImageSpan(this, R.mipmap.ic_topic_detail_jinghao_black)
spanStr.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// corner
spanStr.append(" #")
val len = spanStr.length
val cornerSpan = CenteredImageSpan(this, R.mipmap.ic_topic_detail_remen)
spanStr.setSpan(cornerSpan, len - 1, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.ctTalkDetailInfo.talkNameTv.text = spanStr
-

FileProvider 的使用

FileProvider 的使用

Dev Doc

-

https://developer.android.google.cn/reference/androidx/core/content/FileProvider

-

0x01 定义一个FileProvider

在 androidx 包提供的 FileProvider 提供了 生成文件Uri 的功能。

-

在 manifest 文件中,声明一个 provider

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<manifest>
...
<application>
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"
tools:replace="android:resource" />
</provider>
...
</application>
</manifest>
- -

0x02 可用文件路径配置

在 res/xml/file_paths.xml 下配置可用的文件路径,FileProvider 只能生成配置了的文件Uri。每个你想要生成Uri的文件路径都需要在 paths 下面定义。

-
1
2
3
4
5
6
7
8
9
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
<cache-path name="name" path="path" />
<external-path name="name" path="path" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
<external-media-path name="name" path="path" />
...
</paths>
- -

0x03 生成一个Uri

和其他 app 共享一个文件,你需要生成一个Uri。

-
1
2
3
4
5
6
7
8
9
File imagePath = new File(Context.getFilesDir(), "my_images");
File newFile = new File(imagePath, "default_image.jpg");

Uri uriForFile;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uriForFile = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", newFile);
} else {
uriForFile = Uri.parse("file://" + newFile.toString());
}
- -

getUriForFile() 返回一个 content URI content://com.mydomain.fileprovider/my_images/default_image.jpg.

-

0x04 授予权限

1
2
3
shareContentIntent.setClipData(ClipData.newRawUri("", contentUri));
shareContentIntent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- -
    -
  1. Put the content URI in an Intent by calling setData().
  2. -
  3. Call the method Intent.setFlags() with either Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or both.
  4. -
  5. Send the Intent to another app. Most often, you do this by calling setResult().
  6. -
-

0x05 提供Uri给其他app

1
2
3
4
5
// 使用uri
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
i.setDataAndType(uriForFile, "application/vnd.android.package-archive");
mContext.startActivity(i);
- -

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.
+

FileProvider 的使用

FileProvider 的使用

Dev Doc

+

https://developer.android.google.cn/reference/androidx/core/content/FileProvider

+

0x01 定义一个FileProvider

在 androidx 包提供的 FileProvider 提供了 生成文件Uri 的功能。

+

在 manifest 文件中,声明一个 provider

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<manifest>
...
<application>
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"
tools:replace="android:resource" />
</provider>
...
</application>
</manifest>
+ +

0x02 可用文件路径配置

在 res/xml/file_paths.xml 下配置可用的文件路径,FileProvider 只能生成配置了的文件Uri。每个你想要生成Uri的文件路径都需要在 paths 下面定义。

+
1
2
3
4
5
6
7
8
9
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
<cache-path name="name" path="path" />
<external-path name="name" path="path" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
<external-media-path name="name" path="path" />
...
</paths>
+ +

0x03 生成一个Uri

和其他 app 共享一个文件,你需要生成一个Uri。

+
1
2
3
4
5
6
7
8
9
File imagePath = new File(Context.getFilesDir(), "my_images");
File newFile = new File(imagePath, "default_image.jpg");

Uri uriForFile;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uriForFile = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", newFile);
} else {
uriForFile = Uri.parse("file://" + newFile.toString());
}
+ +

getUriForFile() 返回一个 content URI content://com.mydomain.fileprovider/my_images/default_image.jpg.

+

0x04 授予权限

1
2
3
shareContentIntent.setClipData(ClipData.newRawUri("", contentUri));
shareContentIntent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ +
    +
  1. Put the content URI in an Intent by calling setData().
  2. +
  3. Call the method Intent.setFlags() with either Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or both.
  4. +
  5. Send the Intent to another app. Most often, you do this by calling setResult().
  6. +
+

0x05 提供Uri给其他app

1
2
3
4
5
// 使用uri
Intent i = new Intent(Intent.ACTION_VIEW);
i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
i.setDataAndType(uriForFile, "application/vnd.android.package-archive");
mContext.startActivity(i);
+ +

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.

问题分析

    @@ -181,14 +203,7 @@

    https://github.com/android-notes/Cockroach

    https://github.com/android-notes/Cockroach/blob/master/%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md

    -

Android专栏-SmartRefreshLayout

Android专栏-SmartRefreshLayout

0x01 加载结束之后底部多出一段空白位置

SmartRefreshLayout 嵌套 ViewPager2 上拉加载更多,在 finishLoadMore() 方法之后,底部加载 Loading 位置会多出一段空白不消失。

-

解决方案:

-
1
smartRefreshLayout.setEnableScrollContentWhenLoaded(false)
- -

0x02 下拉刷新+PAG动画

自定义下拉刷新头部,使用 PAGView 做动画,可以在 onMoving(boolean b, float v, int i, int i1, int i2) 方法中设置 pagView.setProgress(v); 添加手势动画。

-
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import com.scwang.smartrefresh.layout.api.RefreshHeader;
import com.scwang.smartrefresh.layout.api.RefreshKernel;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.constant.RefreshState;
import com.scwang.smartrefresh.layout.constant.SpinnerStyle;

import org.libpag.PAGFile;
import org.libpag.PAGView;

public class CommonRefreshHeader extends RelativeLayout implements RefreshHeader {
public static final String DEFAULT_LOADING_FILE = "load_bubble.pag";

protected View mView;
protected ImageView sdv_background;
private int mFinishDuration = 300;
private ViewGroup rootLayout;
private PAGView pagView;
private TextView toastTv;

public CommonRefreshHeader(Context context) {
super(context);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}

public int getLayoutId() {
return R.layout.layout_refresh_head;
}

protected void initView(Context context) {
mView = LayoutInflater.from(context).inflate(getLayoutId(), this);
toastTv = mView.findViewById(R.id.tv_toast);
rootLayout = mView.findViewById(R.id.rootLayout);
sdv_background = mView.findViewById(R.id.sdv_background);
PAGFile file = PAGFile.Load(context.getAssets(), DEFAULT_LOADING_FILE);
pagView = new PAGView(getContext());
LayoutParams params = new LayoutParams(UiUtils.dip2px(39), UiUtils.dip2px(39));
params.addRule(RelativeLayout.CENTER_IN_PARENT);
pagView.setLayoutParams(params);
pagView.setFile(file);
pagView.setRepeatCount(0);
rootLayout.addView(pagView);
}

public void setMarginTop(int marginTop) {
if (mView == null) return;
ViewGroup root = mView.findViewById(R.id.rootLayout);
if (root != null && root.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams params = (MarginLayoutParams) root.getLayoutParams();
params.topMargin = marginTop;
root.setLayoutParams(params);
}
}

@NonNull
@Override
public View getView() {
return mView;
}

@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.Translate;
}

@Override
public void setPrimaryColors(@ColorInt int... colors) {

}

@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {

}

@Override
public void onMoving(boolean b, float v, int i, int i1, int i2) {
/**
* 【仅限框架内调用】手指拖动下拉(会连续多次调用,添加isDragging并取代之前的onPulling、onReleasing)
* @param isDragging true 手指正在拖动 false 回弹动画
* @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (footerHeight+maxDragHeight)
* @param height 高度 HeaderHeight or FooterHeight (offset 可以超过 height 此时 percent 大于 1)
* @param maxDragHeight 最大拖动高度 offset 可以超过 height 参数 但是不会超过 maxDragHeight
*/
if (pagView != null && !pagView.isPlaying()) {
pagView.setProgress(v);
pagView.flush();
}
}

@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int i, int i1) {

}

@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {

}

@Override
public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) {
LogUtils.e("RefreshHeader", "onStartAnimator");
if (pagView != null) pagView.play();
}

@Override
public int onFinish(RefreshLayout layout, boolean success) {
LogUtils.e("RefreshHeader", "onFinish");
if (pagView != null) pagView.stop();
return mFinishDuration;//延迟500毫秒之后再弹回
}

@Override
public boolean isSupportHorizontalDrag() {
return false;
}

@Override
public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) {
switch (newState) {
case None:
case PullDownToRefresh:
case Refreshing:
if (pagView != null) pagView.setVisibility(VISIBLE);
if (toastTv != null) toastTv.setVisibility(GONE);
break;
case ReleaseToRefresh:
break;
case RefreshFinish:
if (pagView != null) pagView.setVisibility(GONE);
if (toastTv != null) toastTv.setVisibility(VISIBLE);
break;
}
}

public void setToastText(String str) {
if (StringUtils.isEmpty(str)) return;
if (toastTv != null) {
toastTv.setText(str);
}
}

public void setFinishDuration(int finishDuration) {
this.mFinishDuration = finishDuration;
}
}
- -

Gson 数据解析

Gson 数据解析

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

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

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

0x02 手动解析Gson基础字段

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

+

Android专栏-SmartRefreshLayout

Android专栏-SmartRefreshLayout

0x01 加载结束之后底部多出一段空白位置

SmartRefreshLayout 嵌套 ViewPager2 上拉加载更多,在 finishLoadMore() 方法之后,底部加载 Loading 位置会多出一段空白不消失。

+

解决方案:

+
1
smartRefreshLayout.setEnableScrollContentWhenLoaded(false)
+ +

0x02 下拉刷新+PAG动画

自定义下拉刷新头部,使用 PAGView 做动画,可以在 onMoving(boolean b, float v, int i, int i1, int i2) 方法中设置 pagView.setProgress(v); 添加手势动画。

+
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import com.scwang.smartrefresh.layout.api.RefreshHeader;
import com.scwang.smartrefresh.layout.api.RefreshKernel;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.constant.RefreshState;
import com.scwang.smartrefresh.layout.constant.SpinnerStyle;

import org.libpag.PAGFile;
import org.libpag.PAGView;

public class CommonRefreshHeader extends RelativeLayout implements RefreshHeader {
public static final String DEFAULT_LOADING_FILE = "load_bubble.pag";

protected View mView;
protected ImageView sdv_background;
private int mFinishDuration = 300;
private ViewGroup rootLayout;
private PAGView pagView;
private TextView toastTv;

public CommonRefreshHeader(Context context) {
super(context);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}

public int getLayoutId() {
return R.layout.layout_refresh_head;
}

protected void initView(Context context) {
mView = LayoutInflater.from(context).inflate(getLayoutId(), this);
toastTv = mView.findViewById(R.id.tv_toast);
rootLayout = mView.findViewById(R.id.rootLayout);
sdv_background = mView.findViewById(R.id.sdv_background);
PAGFile file = PAGFile.Load(context.getAssets(), DEFAULT_LOADING_FILE);
pagView = new PAGView(getContext());
LayoutParams params = new LayoutParams(UiUtils.dip2px(39), UiUtils.dip2px(39));
params.addRule(RelativeLayout.CENTER_IN_PARENT);
pagView.setLayoutParams(params);
pagView.setFile(file);
pagView.setRepeatCount(0);
rootLayout.addView(pagView);
}

public void setMarginTop(int marginTop) {
if (mView == null) return;
ViewGroup root = mView.findViewById(R.id.rootLayout);
if (root != null && root.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams params = (MarginLayoutParams) root.getLayoutParams();
params.topMargin = marginTop;
root.setLayoutParams(params);
}
}

@NonNull
@Override
public View getView() {
return mView;
}

@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.Translate;
}

@Override
public void setPrimaryColors(@ColorInt int... colors) {

}

@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {

}

@Override
public void onMoving(boolean b, float v, int i, int i1, int i2) {
/**
* 【仅限框架内调用】手指拖动下拉(会连续多次调用,添加isDragging并取代之前的onPulling、onReleasing)
* @param isDragging true 手指正在拖动 false 回弹动画
* @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (footerHeight+maxDragHeight)
* @param height 高度 HeaderHeight or FooterHeight (offset 可以超过 height 此时 percent 大于 1)
* @param maxDragHeight 最大拖动高度 offset 可以超过 height 参数 但是不会超过 maxDragHeight
*/
if (pagView != null && !pagView.isPlaying()) {
pagView.setProgress(v);
pagView.flush();
}
}

@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int i, int i1) {

}

@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {

}

@Override
public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) {
LogUtils.e("RefreshHeader", "onStartAnimator");
if (pagView != null) pagView.play();
}

@Override
public int onFinish(RefreshLayout layout, boolean success) {
LogUtils.e("RefreshHeader", "onFinish");
if (pagView != null) pagView.stop();
return mFinishDuration;//延迟500毫秒之后再弹回
}

@Override
public boolean isSupportHorizontalDrag() {
return false;
}

@Override
public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) {
switch (newState) {
case None:
case PullDownToRefresh:
case Refreshing:
if (pagView != null) pagView.setVisibility(VISIBLE);
if (toastTv != null) toastTv.setVisibility(GONE);
break;
case ReleaseToRefresh:
break;
case RefreshFinish:
if (pagView != null) pagView.setVisibility(GONE);
if (toastTv != null) toastTv.setVisibility(VISIBLE);
break;
}
}

public void setToastText(String str) {
if (StringUtils.isEmpty(str)) return;
if (toastTv != null) {
toastTv.setText(str);
}
}

public void setFinishDuration(int finishDuration) {
this.mFinishDuration = finishDuration;
}
}
+ +

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 方法中计算浏览的时长,同时上报浏览的进度。

@@ -94,7 +101,7 @@

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)
+

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。将

@@ -103,62 +110,48 @@

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

0x02 ViewPager2+FragmentStateAdapter的notifyDataSetChanged方法失效

原因分析

-

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

+

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 +

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

+

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

img

原因分析:

-

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

+

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

解决方案:

-

方案一 自定义NestedScrollableHost

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

+

方案一 自定义 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代码:

+

对应的 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()
}
}
}
}
+

方案二 自定义 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);
}
}
-

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)
+

方法三 使用 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;
}
-

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)
+

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 对象为空,反序列化异常

@@ -176,9 +198,7 @@

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

RecyclerView的几种Decoration

RecyclerView的几种Decoration

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
import android.content.res.Resources
import android.graphics.Rect
import android.util.TypedValue
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class SimplePaddingDecoration(
spaceDp: Int,
val orientation: Int = RecyclerView.VERTICAL
) : RecyclerView.ItemDecoration() {
private val dividerHeight: Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
spaceDp.toFloat(),
Resources.getSystem().displayMetrics
).toInt()

override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition

if (orientation == RecyclerView.VERTICAL) {
// 竖直
if (position + 1 != parent.adapter?.itemCount) {
outRect.set(0, 0, 0, dividerHeight)
} else {
outRect.set(0, 0, 0, 0)
}
} else {
// 水平
if (position + 1 != parent.adapter?.itemCount) {
outRect.set(0, 0, dividerHeight, 0)
} else {
outRect.set(0, 0, 0, 0)
}
}
}
}
- -

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

0x01 系统环境

Windows 10 专业版 64 位操作系统

+

11th Gen Intel(R) Core(TM) i7-11700 @ 2.50GHz (8 核)

+

Intel(R) UHD Graphics 750

+

0x02 安装 python3 和 git

Download and install
git
and
Python 3.10.6
(tick Add to PATH)

+

0x03 安装 pytorch

https://pytorch.org/get-started/locally/

+

由于当前 PC 的显卡不是英伟达,所以下载的 tarch 版本是 cup 版本

+

检测 pytorch 是否安装成功

+
1
2
python # 打开python环境
import torch
+ +

没有报异常即说明安装成功

+

0x04 安装 stable-diffusion-webui

https://github.com/AUTOMATIC1111/stable-diffusion-webui

+

由于当前 PC 的 CUP 和 GPU 都是 Intel,所以下载 openvino 版本

+
1
2
3
git clone https://github.com/openvinotoolkit/stable-diffusion-webui.git
cd stable-diffusion-webui
.\webui-user.bat # 这里会开始下载,并且安装需要的软件,等待完成
+ +

0x05 下载网络上已经训练好的 AI 模型

现在我们还需要一个训练好的 AI 模型来指导生成效果,在如下网站中可以下载到很多训练好的模型:

+

https://civitai.com/

+

选择一个感兴趣的模型,下载好的,将模型文件放入工程目录下的 models/Stable-diffusion 文件夹下面即可。

+

0x06 本地运行

下面可以尝试运行下此 Web 项目,在工程目录下执行:

+
1
.\webui-user.bat
+ +

执行完成后,在浏览器打开如下地址:

+

http://127.0.0.1:7860/

+

0x07 自定义 OpenVINO 脚本

https://github.com/openvinotoolkit/stable-diffusion-webui/wiki/Custom-Scripts#accelerate-with-openvino

+

选择自定义脚本 Accelerate with OpenVINO,按照文档配置相关参数。之后就可体验 stable-diffusion 生成图片了。

+

Good Luck.

+

参考链接:

https://huishao.cc/2023/07/30/478Mac%E9%83%A8%E7%BD%B2AIGC%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90%E6%9C%8D%E5%8A%A1%E2%80%94%E2%80%94%E5%9F%BA%E4%BA%8Estable-diffusion/

+
Your browser is out-of-date!

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

×

\ No newline at end of file diff --git a/tags/Android/index.html b/tags/Android/index.html index f7617e49..f96c0bae 100644 --- a/tags/Android/index.html +++ b/tags/Android/index.html @@ -679,7 +679,7 @@

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)
+

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。将

@@ -688,39 +688,47 @@

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

0x02 ViewPager2+FragmentStateAdapter的notifyDataSetChanged方法失效

原因分析

-

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

+

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 +

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

+

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

img

原因分析:

-

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

+

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

解决方案:

-

方案一 自定义NestedScrollableHost

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

+

方案一 自定义 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代码:

+

对应的 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);
}
}
+

方案二 自定义 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)
-

方法三 使用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()
}
}
}
}
+

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)

问题分析

@@ -929,7 +937,7 @@

1
2
Log.i(TAG, BuildConfig.BUILD_TIME);
Log.i(TAG, getString(R.string.build_time));

0x10 设定编码

1
2
3
4
5
allprojects {
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}
}
-

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
}
}
-

JsBridge 开源库

JsBridge 开源库

项目地址:https://github.com/lzyzsd/JsBridge
使用参考:https://www.jianshu.com/p/7aea03838f19

0x00 从H5界面,跳转Native登录,登录之后重新加载H5页面出现JsBridge注入失败[code=-2,message=net::ERR_NAME_NOT_RESOLVED]

解决方案:
App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js-bridge register error
// 回调 onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
// [code=-2,message=net::ERR_NAME_NOT_RESOLVED]
// so you have to destroy webView and rebuild one.
private void reloadWebView() {
if (mWebView != null) {
mWebView.destroy();
mWebView = null;
}

initView();
initWebView();
webView.loadUrl(url);
}
-

多线程编程

多线程编程

0x01 生产者和消费者问题

1.BlockingQueue 阻塞队列实现

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
// 生产者
class Producer implements Runnable {
private final BlockingQueue<Integer> queue;

public Producer(BlockingQueue queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
try {
queue.put(produce());
System.out.println(Thread.currentThread().getName() + " put a message, total = " + queue.size());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private int produce() {
int x = (int) (Math.random() * 10);
System.out.println(Thread.currentThread().getName() + " put: " + x);
return x;
}
}
阅读更多

Java内存模型JMM

Java内存模型

java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的。

-

0x01.主内存与工作内存

java内存模型规定了所有的变量都存储在住内存。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的主内存中的变量的拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量传递均需要通过主内存来完成。当多个线程操作的变量涉及到同一个主内存区域,将可能导致各自的工作线程数据不一致,这样就导致变量同步回主内存的时候可能冲突导致数据丢失。

阅读更多

JsBridge 开源库

JsBridge 开源库

项目地址:https://github.com/lzyzsd/JsBridge
使用参考:https://www.jianshu.com/p/7aea03838f19

0x00 从H5界面,跳转Native登录,登录之后重新加载H5页面出现JsBridge注入失败[code=-2,message=net::ERR_NAME_NOT_RESOLVED]

解决方案:
App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// js-bridge register error
// 回调 onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
// [code=-2,message=net::ERR_NAME_NOT_RESOLVED]
// so you have to destroy webView and rebuild one.
private void reloadWebView() {
if (mWebView != null) {
mWebView.destroy();
mWebView = null;
}

initView();
initWebView();
webView.loadUrl(url);
}
-

Python爬取网络图片

直接上源码

-
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
'''
用Python爬某新闻网站的照片
PS:仅测试爬虫功能,滥用后果自负
'''
import requests
from hashlib import md5
import re
import os

WEB_URL = 'https://new.qq.com/omn/20200207/20200207A0OSQX00.html'
IMAGE_PATH='/Users/**/workspaces/pythonP/img1'

# 通过正则找出网页中所有的图片地址
def find_img_url():
r = requests.get(WEB_URL)
r.raise_for_status()
r.encoding = r.apparent_encoding
demo = r.text
list = []
# ".*?" :正则表达式匹配任意字符串
pattern1 = '<img src=".*?" class="content-picture">'
results1 = re.findall(pattern1, demo)
for res1 in results1:
# <img src="//inews.gtimg.com/newsapp_bt/0/11299639206/1000" class="content-picture">
# https://inews.gtimg.com/newsapp_bt/0/11299639205/1000
res1 = res1.replace('<img src=\"','https:')
res1 = res1.replace('\" class=\"content-picture\">','')
list.append(res1)
# print(res1)
pattern2 = '\"http://inews.gtimg.com/newsapp_bt/0/.*?/1000\"'
results2 = re.findall(pattern2, demo)
for res2 in results2:
res2 = res2.replace('\"','')
list.append(res2)
# print(res2)
return list

# 下载图片到本地
def download_image(img_url):
r = requests.get(img_url)
r.raise_for_status()
content = r.content
file_path = '{0}/{1}.{2}'.format(IMAGE_PATH, md5(content).hexdigest(), 'jpg')
# print(file_path)
if not os.path.exists(file_path):#os.path.exists(file_path)判断文件是否存在,存在返回1,不存在返回0
with open(file_path, 'wb') as f:
f.write(content)
f.close()

list = find_img_url()
for i in list:
print(i, ' download...')
download_image(i)
print("一共下载{}条数据!".format(len(list)))

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

Windows 部署 AIGC 图片生成服务——基于 stable-diffusion

0x01 系统环境

Windows 10 专业版 64 位操作系统

+

11th Gen Intel(R) Core(TM) i7-11700 @ 2.50GHz (8 核)

+

Intel(R) UHD Graphics 750

+

0x02 安装 python3 和 git

Download and install
git
and
Python 3.10.6
(tick Add to PATH)

+

0x03 安装 pytorch

https://pytorch.org/get-started/locally/

+

由于当前 PC 的显卡不是英伟达,所以下载的 tarch 版本是 cup 版本

+

检测 pytorch 是否安装成功

+
1
2
python # 打开python环境
import torch
+ +

没有报异常即说明安装成功

+

0x04 安装 stable-diffusion-webui

https://github.com/AUTOMATIC1111/stable-diffusion-webui

+

由于当前 PC 的 CUP 和 GPU 都是 Intel,所以下载 openvino 版本

+
1
2
3
git clone https://github.com/openvinotoolkit/stable-diffusion-webui.git
cd stable-diffusion-webui
.\webui-user.bat # 这里会开始下载,并且安装需要的软件,等待完成
+ +

0x05 下载网络上已经训练好的 AI 模型

现在我们还需要一个训练好的 AI 模型来指导生成效果,在如下网站中可以下载到很多训练好的模型:

+

https://civitai.com/

+

选择一个感兴趣的模型,下载好的,将模型文件放入工程目录下的 models/Stable-diffusion 文件夹下面即可。

+

0x06 本地运行

下面可以尝试运行下此 Web 项目,在工程目录下执行:

+
1
.\webui-user.bat
+ +

执行完成后,在浏览器打开如下地址:

+

http://127.0.0.1:7860/

+

0x07 自定义 OpenVINO 脚本

https://github.com/openvinotoolkit/stable-diffusion-webui/wiki/Custom-Scripts#accelerate-with-openvino

+

选择自定义脚本 Accelerate with OpenVINO,按照文档配置相关参数。之后就可体验 stable-diffusion 生成图片了。

+

Good Luck.

+

参考链接:

https://huishao.cc/2023/07/30/478Mac%E9%83%A8%E7%BD%B2AIGC%E5%9B%BE%E7%89%87%E7%94%9F%E6%88%90%E6%9C%8D%E5%8A%A1%E2%80%94%E2%80%94%E5%9F%BA%E4%BA%8Estable-diffusion/

+
Your browser is out-of-date!

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

×

\ No newline at end of file diff --git "a/tags/\344\270\213\350\275\275/index.html" "b/tags/\344\270\213\350\275\275/index.html" index 2080472b..24261d14 100644 --- "a/tags/\344\270\213\350\275\275/index.html" +++ "b/tags/\344\270\213\350\275\275/index.html" @@ -84,7 +84,7 @@

遍历TaskRecord中所有的ThreadRecord.isComplete就认为下载完成

0x04 M3U8文件下载过程

M3U8ThreadTaskAdapter.readDynamicFile(InputStream is) // 动态长度文件读取方式
M3U8ThreadTaskAdapter.handleComplete() // 处理完成
ThreadTask.updateCompleteState() // 组装Message消息,发送给VodStateManager.callback -> Handler.Callback
VodStateManager.callback -> STATE_COMPLETE
BaseListener.onComplete() // 对应的实体类是M3U8Listener

-

ARouter源码解析

ARouter源码解析

0x01 init ()

ARouter 的入口,初始化SDK ARouter.init(mApplication);

1
2
3
4
5
6
7
8
9
10
11
public static void init(Application application) {
if (!hasInit) { // 避免重复初始化
logger = _ARouter.logger; // 日志初始化
_ARouter.logger.info(Consts.TAG, "ARouter init start.");
hasInit = _ARouter.init(application); // 正式初始化
if (hasInit) { // 初始化之后
_ARouter.afterInit(); // 管理拦截器的服务 InterceptorService 初始化
}
_ARouter.logger.info(Consts.TAG, "ARouter init over.");
}
}
-

_ARouter.init()

阅读更多