百度程序员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开发小技巧

Android的View是树形结构的,View可能会堆叠在一起,当点击的当地有多个View能够呼应点击事情时,为了确定该让哪个View处理这次点击事情,就需求事情分发机制来帮助。事情搜集之后最先传递给 Activity,然后依次向下传递,大致如下:Activity -> PhoneWindow -> DecorView -> ViewGroup -> … -> View。假如没有任何View消费掉事情,那么这个事情会按照反方向回传,最终传回给Activity,假如最终 Activity 也没有处理,本次事情才会被抛弃。这是一个非常典型的职责链形式。整个进程,有三个非常重要的办法:

百度程序员Android开发小技巧

以上三个办法均有一个布尔类型的回来值,经过回来 true 和 false 来操控事情传递的流程。这三个办法的调用关系,能够用下面的伪代码描绘:

publicbooleandispatchTouchEvent(MotionEventev){
    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官方供给的一个解决方案,能够比较优雅地解决这个问题,如下描绘:

Helper class to handle situations where you want a view to have a larger touch area than its actual view bounds. The view whose touch area is changed is called the delegate view. This class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an instance that specifies the bounds that should be mapped to the delegate and the delegate view itself.

当然,假如运用 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的点击区域的意图。

publicstaticvoidexpandTouchArea(Viewancestor,Viewchild,intleft,inttop,intright,intbottom){
if(child!=null&&ancestor!=null){
MyTouchDelegatetouchDelegate;
if(ancestor.getTouchDelegate()instanceofMyTouchDelegate){
touchDelegate=(MyTouchDelegate)ancestor.getTouchDelegate();
touchDelegate.addExpandChild(child,left,top,right,bottom);
}else{
touchDelegate=newMyTouchDelegate(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;
……

推荐阅读【技术加油站】系列:

人工智能超大规模预练习模型浅谈

揭秘百度智能测验在测验主动生成范畴的探究

小程序主动化测验框架原理分析