百度程序员Android开发小技巧

逆水行舟用力撑,一篙松劲退千寻。这篇文章主要讲述百度程序员Android开发小技巧相关的知识,希望能为你提供帮助。

百度程序员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()) returnguideMap.forEach if (entry.value.isShowing().invoke()) returnguideModel.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 setOnTouchListenerv, event -> when (event.action) MotionEvent.ACTION_DOWN -> v.alpha = pressedAlpha MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> v.alpha = 1.0f// 注意这里要return false false

用户对屏幕的操作,可以简单划分为以下几个最基础的事件:
百度程序员Android开发小技巧

文章图片

Android的View是树形结构的,View可能会重叠在一起,当点击的地方有多个View可以响应点击事件时,为了确定该让哪个View处理这次点击事件,就需要事件分发机制来帮忙。事件收集之后最先传递给 Activity,然后依次向下传递,大致如下:Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View。如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃。这是一个非常典型的责任链模式。整个过程,有三个非常重要的方法:
百度程序员Android开发小技巧

文章图片

以上三个方法均有一个布尔类型的返回值,通过返回 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开发小技巧】当然,如果使用 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; ……

推荐阅读【技术加油站】系列:
人工智能超大规模预训练模型浅谈
揭秘百度智能测试在测试自动生成领域的探索
小程序自动化测试框架原理剖析

    推荐阅读