百度程序员Android开发小技巧
Posted 百度Geek说
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了百度程序员Android开发小技巧相关的知识,希望对你有一定的参考价值。
本期技术加油站给大家带来百度一线的同学在日常工作中android 开发的小技巧:Android有序管理功能引导;一行代码给View增加按下态;一行代码扩大 Andriod 点击区域,希望能为大家的技术提升助力!
01Android有序管理功能引导
随着移动互联网的发展,APP的迭代进入了深水区,产品迭代越来越精细化。很多新需求都会添加功能引导,提高用户对新功能的感知。但是,如果每个功能引导都不考虑其它的功能引导View冲突,就会出现多个引导同时出现的情况,非常影响用户体验,降低引导效果。因此,有序管理功能引导View就显得非常重要。
首先,我们需要根据自身的业务场景,梳理不同的引导类型。为了精准区分每一种引导,使用枚举定义。
enum class GuideType
GuideTypeA,
...
GuideTypeN
其次,将这些引导注册到引导管理器GuideManager中,注册方法需要传入引导的类型,显示引导回调,引导是否正在显示回调,引导是否已经显示回调等参数。注册引导实际上就是将引导的根据优先级保存在一个集合中,便于在需要显示引导时,判断此时是否能够显示该引导。
object GuideManager
private val guideMap = mutableMapOf<Int, GuideModel>()
fun registerGuide(guideType: GuideType,
show: () -> Unit,
isShowing: () -> Boolean,
hasShown: () -> Boolean,
setHasShown: () -> Unit)
guideMap[guideType.ordinal] = GuideModel(show, isShowing, hasShown, setHasShown)
...
接下来,业务方调用GuideManager.show(guideType)触发引导的显示。
-
如果要显示的引导没有注册,则不会显示;
-
如果要显示的引导正在显示或已经显示,则不会重复显示;
-
如果当前注册的引导集合中有引导正在显示,则不会显示;
- 调用show回调,设置已经显示过;
object GuideManager
...
fun show(guideType: GuideType)
val guideModel = guideMap[guideType.ordinal] ?: return
if (guideModel.isShowing.invoke() || guideModel.hasShown.invoke())
return
guideMap.forEach
if (entry.value.isShowing().invoke())
return
guideModel.run
show().invoke()
setHasShown().invoke()
最后,需要处理单例中已注册引导的释放逻辑,将guideMap集合清空。
object GuideManager
...
fun release()
guideMap.clear()
以上实现是简易版的引导管理器,使用时还可以结合具体业务场景,添加更多的引导拦截策略,例如当前业务场景处于某个状态时,所有引导都不展示,则可以在GuideManager.show(guideType)中添加个性化处理逻辑。
02一行代码给View增加按下态
在Android开发中,经常会遇到UE要求添加按下态效果。常规的写法是使用selector,分别设置按下态和默认态的资源,代码示例如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/XX_pressed" android:state_selected="true"/>
<item android:drawable="@drawable/XX_pressed" android:state_pressed="true"/>
<item android:drawable="@drawable/XX_normal"/>
</selector>
UE提供的按下态效果,有的时候仅需改变透明度。这种效果也可以用上述方法实现,但缺点也很明显,需要增加额外的按下态资源,影响包体积。这个时候我们可以使用alpha属性,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/XX" android:alpha="XX" android:state_selected="true"/>
<item android:drawable="@drawable/XX" android:alpha="XX" android:state_pressed="true"/>
<item android:drawable="@drawable/XX"/>
</selector>
这种写法,不需要额外增加按下态资源,但也有一些缺点:该属性Android 6.0以下不生效。
我们可以利用Android的事件分发机制,封装一个工具类,从而达到一行代码实现按下态。代码如下:
@JvmOverloads
fun View.addPressedState(pressedAlpha: Float = 0.2f) = run
setOnTouchListener v, event ->
when (event.action)
MotionEvent.ACTION_DOWN -> v.alpha = pressedAlpha
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> v.alpha = 1.0f
// 注意这里要return false
false
用户对屏幕的操作,可以简单划分为以下几个最基础的事件:
Android的View是树形结构的,View可能会重叠在一起,当点击的地方有多个View可以响应点击事件时,为了确定该让哪个View处理这次点击事件,就需要事件分发机制来帮忙。事件收集之后最先传递给 Activity,然后依次向下传递,大致如下:Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View。如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃。这是一个非常典型的责任链模式。整个过程,有三个非常重要的方法:
以上三个方法均有一个布尔类型的返回值,通过返回 true 和 false 来控制事件传递的流程。这三个方法的调用关系,可以用下面的伪代码描述:
public boolean dispatchTouchEvent(MotionEvent ev)
boolean consume = false;
if (onInterceptTouchEvent(ev))
consume = onTouchEvent(ev);
else
consume = child.dispatchTouchEvent(ev);
return consume;
对于一个View来说,它可以注册很多事件监听器,例如单击事件、长按事件、触摸事件,并且View自身也有onTouchEvent方法,这些与事件相关的方法由View的dispatchTouchEvent方法管理,事件的调度顺序是onTouchListener -> onTouchEvent -> onLongClickListener -> onClickListener。所以我们可以通过为View添加onTouchListener来处理View的按下、抬起效果。需要注意的是,如果onTouchListener中的onTouch返回true,不会再继续执行onTouchEvent,后面的事件都不会响应,所以我们需要在工具类中return false。
03一行代码扩大 Andriod 点击区域
在Android 开发中,经常会遇到扩大某些按钮点击区域的场景,如某个页面关闭按钮比较小,为防止误触或点不到,需要扩大其点击区域。
常见的扩大点击区域的思路有三个:
1. 修改布局。如增加按钮的内padding,或者外面嵌套一层Layout,并在外层Layout设置监听。
2. 自定义事件处理。如在父布局中监听点击事件,并设置各组件的响应点击区域,在对应点击区域里时就转发到对应组件的点击。
3. 使用 Android 官方提供的TouchDelegate 设置点击事件。
其中第一种方式弊端很明显,会增加业务复杂度,降低渲染性能;或者当布局位置不够时,增加padding或添加外层布局就行不通了。
第二种方式可以从根本上扩大点击区域,但是问题依旧明显:编码的复杂度太高,每次扩大点击区域都意味着需要根据实际需求去“重复造轮子”:写一堆获取位置、判定等代码。
第三种方式是Android官方提供的一个解决方案,能够比较优雅地解决这个问题,如下描述:
当然,如果使用 Android 的TouchDelegate,很多时候还不能满足我们需求,比如我们想在一个父(祖先)View 中给多个子 View 扩大点击区域,如在一个互动Bar上有点赞、收藏、评论等按钮。这时可以在自定义TouchDelegate时维护一个View Map,该Map 中保存子View和对应需要扩大的区域,然后在点击转发逻辑里动态计算该点击事件属于哪个子View区域,并进行转发。关键代码如下:
// 已省略无关代码
public class MyTouchDelegate extends TouchDelegate
/** 需要扩大点击区域的子 View 和其点击区域的集合 */
private Map<View, ExpandBounds> mDelegateViewExpandMap = new HashMap<>();
@Override
public boolean onTouchEvent(MotionEvent event)
// ……
// 遍历拿到对应的view和扩大区域,其它逻辑跟原始逻辑类似
for (Map.Entry<View, ExpandBounds> entry : mDelegateViewExpandMap.entrySet())
View child = entry.getKey();
ExpandBounds childBounds = entry.getValue()
// ……
public void addExpandChild(View delegateView, int left, int top, int right, int bottom)
MyTouchDelegate.ExpandBounds expandBounds = new MyouchDelegate.ExpandBounds(new Rect(), left, top, right, bottom);
this.mDelegateViewExpandMap.put(delegateView, expandBounds);
更进一步的,可以写个工具类,或者Kotlin扩展方法,输入需要扩大点击区域的View、祖先View、以及对应的扩大大小,从而达到一行代码扩大一个View的点击区域的目的。
public static void expandTouchArea(View ancestor, View child, int left, int top, int right, int bottom)
if (child != null && ancestor != null)
MyTouchDelegate touchDelegate;
if (ancestor.getTouchDelegate() instanceof MyTouchDelegate)
touchDelegate = (MyTouchDelegate)ancestor.getTouchDelegate();
touchDelegate.addExpandChild(child, left, top, right, bottom);
else
touchDelegate = new MyTouchDelegate(child, left, top, right, bottom);
ancestor.setTouchDelegate(touchDelegate);
注意: TouchDelegate在Android8.0及其以前有个bug,如果需要兼容低版本需要留意下,在通过delegate触发子View点击事件之后,父View自己监听的点击事件就永远无法被触发了,原因在于TouchDelegate中对点击事件转发的处理中(onTouchEvent)对MotionEvent.ACTION_DOWN)有问题,不在点击范围内时,未对mDelegateTargeted变量重置为false,导致父view再也收不到点击事件,无法处理click等操作,相关Android源码如下:
// …… 已省略无关代码
public boolean onTouchEvent(MotionEvent event)
// ……
boolean sendToDelegate = false;
boolean handled = false;
switch (event.getAction())
case MotionEvent.ACTION_DOWN:
Rect bounds = mBounds;
if (bounds.contains(x, y))
mDelegateTargeted = true;
sendToDelegate = true;
// if的判断为false时未重置 mDelegateTargeted 的值为false
break;
// ……
if (sendToDelegate)
// 转发代理view
handled = delegateView.dispatchTouchEvent(event);
return handled;
// ……
如果需要兼容低版本,则可以继承自TouchDelegate,覆写 onTouchEvent方法,在事件不在代理范围内时,重置mDelegateTargeted 和sendToDelegate值为false,如下:
……
if (bounds.contains(x, y))
mDelegateTargeted = true;
sendToDelegate = true;
else
mDelegateTargeted = false;
sendToDelegate = false;
// 或者如9.0之后源码的写法
mDelegateTargeted = mBounds.contains(x, y);
sendToDelegate = mDelegateTargeted;
……
推荐阅读【技术加油站】系列:
以上是关于百度程序员Android开发小技巧的主要内容,如果未能解决你的问题,请参考以下文章