开发笔记-UI

开发笔记主要是记录开发中遇到的一些问题和解决方案,以及引发的一些思考,不会太深入地记录问题,但是会尽可能广泛地记录涉及到的内容,方便之后整理归纳和查阅

No.1 RecyclerView 隐藏 item(多布局)的所在区域显示空白

在 RecyclerView 是多种布局的情况下,需要把 RecyclerView 的其中一个条目 GONE 掉,若只是把内容隐藏,这样就会出现一片空白区域,高度不会改变,解决方法是,隐藏时把 item 的高度宽度设置为 0,需要显示的时候再设置回来

1
2
3
4
5
6
7
8
9
10
11
12
13
if (type == TYPE1) { // 某个类型,反正就是要隐藏的类型或者状态
val param = holder.itemView.layoutParams
param.height = 0
param.width = 0
holder.itemView.layoutParams = param
...
} else { // 其他类型,要显示的类型或者状态,恢复宽高
val param = holder.itemView.layoutParams
param.height = ScreenUtil.dp2px(context, 46F)
param.width = RelativeLayout.LayoutParams.MATCH_PARENT
holder.itemView.layoutParams = param
...
}

No.2 ListView getChildAt() 取得位置错误问题

有时候我们需要获取 ListView 或 RecycleView 的某个 item 的 view 对象来做一些处理,发现使用 getChildAt(position: Int) 这个方法取到的不是想要的 item,而是可视的第 position 位置的 item,也就是说 position 只是从第一个可以看到的 item 算起的,这样就和实际列表中的第 position 个是不一样的,这样就需要使用如下代码来取得实际的位置

1
2
3
val itemPosition = 1 // 想要修改的 item position
val targetPosition = itemPosition - listView.getFirstVisiblePosition() // 实际的目标 item position
val itemView = listView.getChildAt(targetPosition)

这里需要注意的是 itemView 在可视范围上方时,会返回 null,在源码中可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Returns the view at the specified position in the group.
*
* @param index the position at which to get the view from
* @return the view at the specified position or null if the position
* does not exist within the group
*/
public View getChildAt(int index) {
if (index < 0 || index >= mChildrenCount) {
return null;
}
return mChildren[index];
}

所以需要对空值做一下处理

No.3 performClick 和 callOnclick 的区别

View 类的 performClick 和 callOnclick 函数都可以实现点击,不用用户手动点击,直接触发 View 的点击事件。区别有如下两点:

  1. API 等级

    performClick 是在 API 1 中加入,callOnClick 是在 API 15 中加入

  2. 代码实现层面

    看两个方面的代码实现,如下:

    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
    /**
    * Directly call any attached OnClickListener. Unlike {@link #performClick()},
    * this only calls the listener, and does not do any associated clicking
    * actions like reporting an accessibility event.
    *
    * @return True there was an assigned OnClickListener that was called, false
    * otherwise is returned.
    */
    public boolean callOnClick() {
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
    li.mOnClickListener.onClick(this);
    return true;
    }
    return false;
    }

    /**
    * Call this view's OnClickListener, if it is defined. Performs all normal
    * actions associated with clicking: reporting accessibility event, playing
    * a sound, etc.
    *
    * @return True there was an assigned OnClickListener that was called, false
    * otherwise is returned.
    */
    public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
    playSoundEffect(SoundEffectConstants.CLICK);
    li.mOnClickListener.onClick(this);
    result = true;
    } else {
    result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
    }

    从代码中可以看出,callOnClick 是 performClick 的简化版,不包含点击播放声音,不具有辅助功能,辅助功能官方介绍如下:

    许多 Android 用户有不同的能力(限制),这要求他们以不同的方式使用他们的 Android 设备。这些限制包括视力,肢体或与年龄有关,这些限制阻碍了他们看到或充分使用触摸屏,而用户的听力丧失,让他们可能无法感知声音信息和警报,Android 提供了辅助功能的特性和服务帮助这些用户更容易的使用他们的设备,这些功能包括语音合成、触觉反馈、手势导航、轨迹球和方向键导航。Android 应用程序开发人员可以利用这些服务,使他们的应用程序更贴近用户

No.4 android ems 具体意义

在 android 里 setEms() 作用是设置 TextView 的字符宽度,em 是一个印刷排版的单位,表示字宽的单位,em 字面意思为:equal M(和M字符一致的宽度为一个单位)简称 em,而 ems 是 em 的复数表达,所以 ems 和字节什么的都是没关系的,只是和字宽度有关系,而且由于各个手机版本自定义字体等问题,所以设置 ems 意义不大,只能作为参考,当设置该属性后,控件显示的长度就为10个字符的长度,超出的部分将不显示,而且 EditText 的属性,只有在
android:layout_width="wrap_content" 时,才会显示,如果是 android:layout_width="match_parent"
时,则不会有变化

No.5 图片显示上下有空白的解决办法

在使用 ImageView 时,采用不同的 scaleType 属性可以调整图片的缩放类型,但是有时会导致图片上下方出现空白,这时就要设置 android:adjustViewBounds="true"

关于 ImageView 的 adjustViewBounds 属性

取值为 true 时:

Adjust the ImageView’s bounds to preserve the aspect ration of its drawable.

调整 ImageView 的界限来保持图像纵横比不变。
这并不意味着 ImageView 的纵横比就一定和图像的纵横比相同,XML定义里的 android:adjustViewBounds="true" 会将这个 ImageView 的 scaleType 设为 fitCenter,不过这个 fitCenter 会被后面定义的 scaleType 属性覆盖(如果定义了的话),除非在 Java 代码里再次显式调用 setAdjustViewBounds(true)

1. 如果设置的 layout_width 与 layout_height 都是定值

那么设置 adjustViewBounds 是没有效果的,ImageView 将始终是设定的定值的宽高

2. 如果设置的 layout_width 与 layout_height 都是 wrap_content

那么设置 adjustViewBounds 是没有意义的,因为 ImageView 将始终与图片拥有相同的宽高比(但是并不是相同的宽高值,通常都会放大一些)

3. 如果两者中一个是定值,一个是 wrap_content

比如 android:layout_width="100px"android:layout_height="wrap_content" 时,ImageView 的宽将始终是 100px,而高则分两种情况:

  • 当图片的宽小于 100px 时,layout_height 将与图片的高相同,即图片不会缩放,完整显示在 ImageView 中,ImageView 高度与图片实际高度相同,图片没有占满 ImageView,ImageView 中有空白
  • 当图片的宽大于等于 100px 时,此时 ImageView 将与图片拥有相同的宽高比,因此 ImageView 的 layout_height 值为:100 除以图片的宽高比,比如图片是 500X500 的,那么 layout_height 是 100,图片将保持宽高比缩放,完整显示在 ImageView 中,并且完全占满 ImageView

No.6 硬件加速导致画线不显示(有待深入研究,先总结现象)

  • Android 9.0 drawLine drawPath 都可以正常实现画线,抗锯齿,实现虚线
  • Android 9.0 以下
    • drawLine 一定能画出线,但是不能画出虚线 (抗锯齿有效)
    • drawPath 一定能画出虚线,但是大斜率的时候线会消失 (抗锯齿无效) 因为x y值过大,不进行绘制了

No.7 在约束布局中使用 include 标签报错

在约束布局 ConstraintLayout 中引入了一个布局,然后给引入布局添加了底部约束,让它距离底部 8dp,但是引入布局仍然出现在顶部,并报错如下:

1
Layout parameter layout_marginBottom ignored unless both layout_width and layout_height are also specified on <include> tag

在约束布局中引入新的控件或者布局时,若不重新指定一下控件或者布局的宽高,那么给它添加的约束便会失效,给 include 标签中添加上 layout_width 和 layout_height 属性即可

No.8 BottomSheetDialog 输入框输入文字时上下跳动

开发中遇到一个问题,一个 BottomSheetDialog 里面有上下两个输入框,上方的输入框在软键盘弹出时,并没有被软键盘顶着,所以输入时,BottomSheetDialog 表现正常,但是下方的输入框,是被软键盘顶的,在输入时,BottomSheetDialog 中的 TextView 要根据输入的内容作出变化,这时,一旦 TextView 的属性为 wrap_parent ,他的的文字发生变化时,高度宽度随之发生变化,BottomSheetDialog 就会上下跃动,体验比较差,后来发现只要给定 TextView 的高度和宽度,就可以避免这个问题,比较低版本的 Android 系统中似乎不会出现,后续有待排查

No.9 关于 ViewPager 嵌套 RecyclerView,当 RecyclerView 滑动到尽头后,不希望 ViewPager 被连带拖动的情况

有下面两种情况

  1. 当 RecyclerView 外部是一个自定义的 View,你可以在这个自定义的 View 中加入下面的代码,这样,外部可以判断如果 rv 不可滑动,就做一个类似拦截的操作,避免引起滑动(还需要研究 dispatchTouchEvent 相关内容,弄明白这样为什么有效)
    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
    private int lastX = 0;
    private int lastY = 0;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev)
    {
    boolean intercept = super.dispatchTouchEvent(ev);
    switch (ev.getAction())
    {
    case MotionEvent.ACTION_DOWN:
    break;
    case MotionEvent.ACTION_MOVE:
    intercept = needIntercept(ev);
    break;
    default:
    }
    lastX = (int)ev.getX();
    lastY = (int)ev.getY();

    return intercept;
    }

    private boolean needIntercept(MotionEvent ev)
    {
    // 水平滚动距离大于垂直滚动距离则拦截
    float deltaX = ev.getX() - lastX;
    float deltaY = ev.getY() - lastY;
    boolean isHorizontal = Math.abs(deltaX) > Math.abs(deltaY);
    if (isHorizontal)
    {
    if (deltaX > 0)
    {
    // 往右滑动
    return !recyclerView.canScrollHorizontally(-1);
    }
    else
    {
    // 往左滑动
    return !recyclerView.canScrollHorizontally(1);
    }
    }
    else
    {
    return true;
    }
    }
  2. 自定义一个 View 继承 RecyclerView,写如下代码,有点像内部拦截法,需要弄懂 requestDisallowInterceptTouchEvent 相关内容
    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    /*---解决垂直 ViewPager 嵌套水平 RecyclerView 横向滑动到底后不滑动 ViewPager start ---*/
    ViewParent parent = this;
    while(!((parent = parent.getParent()) instanceof ViewPager)); // 循环查找 ViewPager
    parent.requestDisallowInterceptTouchEvent(true);
    return super.dispatchTouchEvent(ev);
    }

No.10 View 的一些踩坑

1. GestureDetector 无反应

使用 GestureDetector 时,无论什么手势,均无反应,解决方法:需要重写 onDown 方法,可看参考文章

2. 界面闪烁

View 的逻辑不能太重,运算不能太多,重复调用的的 path 相关操作要记得 reset,可看参考文章

No.11 SurfaceView 重叠问题

有时候我们会遇到页面内有两个 SurfaceView 的场景,比如有一个视频播放器在上方,下方是一个地图控件。正常使用时问题不大,一旦涉及到一些要设置显隐(View.VISIBLE、View.GONE)以及离开页面又回来的情形时,在某些 Android 高版本手机(测试时用的是)就会有一些异常出现,地图控件的内容会显示在视频播放器上,即使这两个控件本来的位置并不重合,这时候如果再设置一下控件的显隐或者位置(总之就是让它再发生一些布局变化刷新一下),又会恢复正常

初步猜测,经过离开页面又返回的这个过程后,控件位置会产生一些未知变化,然后两个 SurfaceView 显示会出现异常,页面内有这种情况,要注意规避,有以下的一些方案:

  1. 不能用 GONE 和 VISIBLE 控制显隐,需要使用 alpha 来控制(xml 中设置为 gone 都不行);原理就是让空间不存在 GONE 或者 INVISIBLE 这种可能导致异常的状态。但是这样之后,点击事件不好处理,因为以前用了 GONE 和 INVISIBLE 之后,控件就点击不到了,用 alpha 控制的话,还要另外处理
  2. 想办法设置 SurfaceView 的背景色,遮盖掉底部地图不让其透出来;要注意等视频播放器出图像后又把背景色移除掉,否则背景色会一直存在导致视频内容被遮盖,这个方案缺点在于你要控制好设置背景色和移除背景色的时机,否则会有一瞬间出现底部的控件,看起来还是有些怪
  3. 用弹窗尝试改造页面;如果你的地图控件是弹出来的或者是点击后再显示的,这种方案其实是最佳的,可以把它写在另一个弹窗页面内,一方面和页面解耦,另一方面可以避免重叠问题
  4. 还有一种方案是设置 surfaceView.setZOrderOnTop(true);这样可以让你想要放在顶层的 SurfaceView 始终位于顶层,但是要注意这样之后它就是最顶层的,没有其他控件能遮盖它了,看到有文章说再调用一下 surfaceView.setZOrderMediaOverlay(true) 可以解决,然而实际并没有用

No.12 补间动画重置问题

因为公司代码框架的问题,切换 Tab 时,补间动画(也可以叫做 View 动画)没有保留在最后一帧,而是重置到第一帧了,一般情况下好像不会这样,但是现在出现了这个问题又不好改公司代码,所以换属性动画来实现,就解决了问题,这是因为属性动画是基于对象的实际属性进行修改,从而体现出动画,而补间动画只是 View 的一些动画体现,实际上 View 没有变化,所以不知道怎么就被重置了。这里简单记录一下,加深对两种动画之间的区别的认识。


开发笔记-UI
https://enderhoshi.github.io/2024/03/05/开发笔记-UI/
作者
HoshIlIlI
发布于
2024年3月5日
许可协议