百度程序员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开发小技巧的主要内容,如果未能解决你的问题,请参考以下文章

22条常用JavaScript开发小技巧

Android——隐藏输入法的小技巧

JS开发中的一些小技巧和方法

小程序开发另类小技巧 --用户授权篇

JS开发中常用的小技巧

百度API获取位置范围内的周边服务