UI问题汇总
11. ScrollView嵌套LinearLayout 水平滑动设置不生效
- 在 Android 中,若要实现水平滑动效果,直接使用 ScrollView 是不行的,因为 ScrollView 仅支持垂直滚动。你需要使用 HorizontalScrollView 来实现水平滑动功能。
- 若直接把 LinearLayout 的 orientation 设为 horizontal 然后嵌套在 ScrollView 里,是无法实现水平滑动的。
- 正确示例
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
...
</LinearLayout>
</HorizontalScrollView>
// 或
<androidx.constraintlayout.widget.ConstraintLayout ...>
<HorizontalScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layout_constraintStart_toEndOf="@id/xxx_left"
app:layout_constraintEnd_toStartOf="@id/xxx_right"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
...
</LinearLayout>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
10. 使用EditText#setFilters(filter)设置过滤规则时,某些键盘自带联想词功能会将前序输入字母拼接联想,处理不当会导致输入框输入字符逐渐累加
例:输入字母a,接着输入b,这时输入框内容会变为aab,接着输入c,输入框内容变成aababc
- 修复方案:可通过InputConnection实现过滤字符效果
class LimitEditText(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0):
AppCompatEditText(context, attrs, defStyleAttr) {
constructor(context: Context): this(context, null)
constructor(context: Context, attrs: AttributeSet? = null): this(context, attrs, 0)
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
return super.onCreateInputConnection(outAttrs)?.let { LimitInputConnection(it, false) }
}
inner class LimitInputConnection(target: InputConnection, mutable: Boolean):
InputConnectionWrapper(target, mutable), InputConnection {
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
/**
* 限制字符
*/
val result = text?.toString()?:""
val isChinese = Pattern.matches("[\u4e00-\u9fa5]", result)
if (!Character.isLetterOrDigit(result.toCharArray()[0]) || isChinese || result.contains("\n") || result.contains("\r")) {
return false
}
return super<InputConnectionWrapper>.commitText(text, newCursorPosition)
}
}
}
9. 在Adapter中使用Glide加载图片上下文为ImageView时,当有其他View层级在该View之上显示时,显示异常问题
- 原因Glide绑定View的生命周期,导致图片回收无法显示
- 修改其生命周期绑定
8. ViewPager2嵌套RecyclerView,ItemView拖拽事件冲突问题
- 解决思路
-
- RecyclerView的滑动冲突,可通过重写LayouManager相关方法解决
-
- ViewPager2的滑动冲突,可通过禁用ViewPager2的滑动事件解决
-
// ViewPager2中
override fun enablePager2(enable: Boolean) {
// true滑动,false禁止滑动
binding.pager2.isUserInputEnabled = enable
}
//RecyclerView中
class TabPagerImage(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
constructor(context: Context): this(context, null)
constructor(context: Context, attrs: AttributeSet? = null): this(context, attrs, 0)
private val tabImageAdapter: TabImageAdapter
var listener: OnTabImageListener? = null
private val binding by lazy {
TabPagerImageBinding.inflate(LayoutInflater.from(context), this, true)
}
init {
tabImageAdapter = TabImageAdapter(mutableListOf(), { dx, dy ->
}, { preview ->
// 回调中禁用启用ViewPager2的滑动事件
if (preview == null) {
listener?.enablePager2(true)
...
} else {
listener?.enablePager2(false)
...
}
})
// RecyclerView以GridLayoutManager为例
binding.tabImageRv.layoutManager = object : GridLayoutManager(context, 3) {
override fun canScrollVertically(): Boolean {
return !tabImageAdapter.disableDragging && super.canScrollVertically()
}
}
}
}
// Adapter中
@SuppressLint("ClickableViewAccessibility")
class TabImageAdapter(
private val datasource: MutableList<TabContent>,
val movePreview: (Float, Float) -> Unit,
val preview: (ImageView?) -> Unit,
): Adapter<TabImageAdapter.Holder>() {
var disableDragging: Boolean = false
inner class Holder(val binding: ItemTabImageBinding): ViewHolder(binding.root) {
init {
binding.itemTabImage.setOnTouchListener { v, event ->
val index = event.actionIndex
val pointerId = event.getPointerId(index)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
if (pointerId != 0) {
return@setOnTouchListener false
}
disableDragging = false
preview(v as ImageView)
dx = event.x
dy = event.y
}
MotionEvent.ACTION_MOVE -> {
disableDragging = true
movePreview(itemView.x + event.x - dx, itemView.y + event.y - dy)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_CANCEL -> {
if (pointerId != 0) {
return@setOnTouchListener false
}
preview(null)
}
}
true
}
}
}
}
7. ViewPager2设置Adapter时报错
- java.lang.IllegalStateException: Pages must fill the whole ViewPager2 (use match_parent)
- 原因
- 通过源码分析设置item时,要求宽高为MATCH_PARENT
/**
* A lot of places in code rely on an assumption that the page fills the whole ViewPager2.
*
* TODO(b/70666617) Allow page width different than width/height 100%/100%
*/
private RecyclerView.OnChildAttachStateChangeListener enforceChildFillListener() {
return new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutParams.width != LayoutParams.MATCH_PARENT || layoutParams.height != LayoutParams.MATCH_PARENT) {
throw new IllegalStateException("Pages must fill the whole ViewPager2 (use match_parent)");
}
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
// nothing
}
};
}
- 修复
- 在Adapter的onCreateViewHolder中设置layoutParams
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabContentAdapter.Holder {
val page = pages[viewType]
val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
page.layoutParams = lp
return Holder(page)
}
6. 自定义View中注册监听问题(例EventBus注册)
- 需在
onAttachedToWindow注册,onDetachedFromWindow取消注册 - 若在构造方法中注册,
onDetachedFromWindow取消注册,则可能会引入OOM问题(内存泄露)
原因
onDetachedFromWindow有场景会不执行,则无法取消注册导致OOMonDetachedFromWindow在View中为空实现,是通过View#dispatchDetachedFromWindow调用,而其又是通过ViewRootImpl#dispatchDetachedFromWindow调用
# ViewRootImpl#dispatchDetachedFromWindow
fun dispatchDetachedFromWindow() {
...
// 当 mView 和 mView.mAttachInfo 不为 null 时,dispatchDetachedFromWindow 才会被调用
if (mView != null && mView.mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
// dispatchDetachedFromWindow 方法在这里被调用
mView.dispatchDetachedFromWindow();
}
...
}
mView创建时机是在ViewRootImpl#setView
# ViewRootImpl#setView
fun setView(view: View, attrs: WindowManager.LayoutParams, panelParentView: View, userId: Int) {
synchronized (this) {
if (mView == null) {
// 在 ViewRootImpl 的 setView 中会给 mView 赋值
mView = view;
...
}
}
setView调用时机是在Activity#onResume之后,通过PhoneWindow拿到WindowManagerImpl,WindowManagerImpl#addView->WindowManagerGlobal#addView->ViewRootImpl#setView- 若在
Activity#onResume之前Activity#finish执行,则View#onDetachedFromWindow不会被执行,也就不会取消注册,即导致OOM - 解决方案即
onAttachedToWindow注册,onDetachedFromWindow取消注册,成对出现,要么都执行,要么都不执行
5. EventBus引起的UI更新问题
- 在子线程中使用EventBus发送事件,订阅事件处上报UI更新问题
- 解决方案:订阅事件处指定线程模式
线程模式
- ThreadMode.POSTING:在发送事件线程调用接收方法(默认)
- ThreadMode.MAIN:在主线程调用接收方法
- ThreadMode.BACKGROUND:在后台线程调用接收方法
- ThreadMode.ASYNC:在异步线程调用接收方法
@Subscribe(threadMode = ThreadMode.MAIN)
fun handleEvent(event: XxxEvent) {
...
}
4. 禁用软键盘
class XxxActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_xxx);
// To disable popping-up soft-keyboard
getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
}
}
3. ViewBinding问题
- 布局中使用include
SubLayout必须与include跟布局一致,否则报错,必须包裹一层,否则设置显示隐藏会导致所有控件不可见
<!-- 在activity_main.xml中引入include_tool(include中有merge标签) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<SubLayout>
<include layout="@layout/include_tool"/>
</SubLayout>
</LinearLayout>
<!-- 在MainActivity中使用 -->
// 方式一
val mBinding = ActivityMainBinding.inflate(layoutInflater)
val mToolBinding = IncludeToolBinding.bind(mBinding.root)
mToolBinding.toolPen.setOnClickListener {}
// 方式二
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include android:id="@+id/include_tool"
layout="@layout/include_tool"/>
</LinearLayout>
val mBinding = ActivityMainBinding.inflate(layoutInflater)
mBinding.includeTool.toolPen.setOnClickListener {}
2. ViewBinding ViewStub问题
ViewStub必须有一层父布局,否则设置显示隐藏有问题(会导致所有控件不可见)
1. 引入
android {
...
viewBinding {
enabled = true
}
}
2. view_stub_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:id="@+id/view_stub_load"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/view_stub_test"/>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ViewStub
android:id="@+id/view_stub_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/view_stub_test"/>
</FrameLayout>
</FrameLayout>
3. view_stub_test.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:background="@drawable/vector_xxx_bg">
<ImageView
android:id="@+id/xxx"
android:layout_width="32dp"
android:layout_height="32dp"
android:padding="4dp"
android:src="@drawable/vector_xxx"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>
4. ViewStubLayout.kt
class ViewStubLayout(context: Context, attrs: AttributeSet? = null,
defAttr: Int = 0, defRes: Int = 0): FrameLayout(context, attrs, defAttr, defRes) {
constructor(context: Context, attrs: AttributeSet? = null): this(context, attrs, 0)
private var viewStubBinding: ViewStubTestBinding? = null
private var mListener: XListener? = null
private val binding: ViewStubLayoutBinding by lazy {
ViewStubLayoutBinding.inflate(LayoutInflater.from(context), this, false)
}
init {
removeAllViews()
addView(binding.root)
binding.viewStubLoad.setOnCLickListener {
if (viewStubBinding == null) {
val viewStub = binding.viewStubTest
viewStub.setOnInflateListener { _, inflated ->
viewStubBinding = ViewStubTestBinding.bind(inflated)
}
viewStub.inflate()
}
...
}
}
private fun callback(tableTool: Int) {
mListener?.toolSelect(tableTool)
}
fun measureHeight() {
layoutParams?.height = ResourcesUtil.getDimension(R.dimen.dp_48)
}
fun setListener(listener: XListener) {
mListener = listener
}
/**
* 回调接口
*/
interface XListener {
/**
* 工具类型选择
*/
fun toolSelect(eventType: Int)
}
}
1. DataBinding ViewStub问题
1. 引入
android {
...
dataBinding {
enabled = true
}
}
2. view_stub_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ViewStub
android:id="@+id/view_stub_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/view_stub_test"/>
</FrameLayout>
</layout>
3. view_stub_test.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:background="@drawable/vector_xxx_bg">
<ImageView
android:id="@+id/xxx"
android:layout_width="32dp"
android:layout_height="32dp"
android:padding="4dp"
android:src="@drawable/vector_xxx"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>
</layout>
4. ViewStubLayout.kt
class ViewStubLayout(context: Context, attrs: AttributeSet? = null,
defAttr: Int = 0, defRes: Int = 0): FrameLayout(context, attrs, defAttr, defRes) {
constructor(context: Context, attrs: AttributeSet? = null): this(context, attrs, 0)
private lateinit var mViewStubBinding: ViewStubTestBinding
private var mListener: XListener? = null
private val mBinding: ViewStubLayoutBinding by lazy {
ViewStubLayoutBinding.inflate(LayoutInflater.from(context), this, false)
}
init {
removeAllViews()
addView(mBinding.root)
if (!mBinding.viewStubTest.isInflated) {
mBinding.viewStubTest.viewStub?.inflate()?.let {
mViewStubBinding = DataBindingUtil.bind<ViewStubTestBinding>(it)!!
}
}
if (::mViewStubBinding.isInitialized) {
mViewStubBinding.xxx.setOnClickListener {
callback(XXX)
}
}
}
private fun callback(tableTool: Int) {
mListener?.toolSelect(tableTool)
}
fun measureHeight() {
layoutParams?.height = ResourcesUtil.getDimension(R.dimen.dp_48)
}
fun setListener(listener: XListener) {
mListener = listener
}
/**
* 回调接口
*/
interface XListener {
/**
* 工具类型选择
*/
fun toolSelect(eventType: Int)
}
}