BaiduプログラマーのAndroid開発のヒント

写真

テクノロジーガソリンスタンドのこの号では、Baiduの第一線の学生による日常業務におけるAndroid開発のヒントを紹介します。Androidの整然とした管理機能のガイダンス、プレス状態を表示に増やすためのコード行、Andriodクリックを拡張するためのコード行エリア、私はそれが皆を助けることができることを願っています技術的な改善が助けになります!

01Android整然とした管理機能ガイド


モバイルインターネットの発展に伴い、APPのイテレーションは深海域に入り、製品のイテレーションはますます洗練されてきました。多くの新しい要件により、機能ガイダンスが追加され、ユーザーの新機能に対する認識が向上します。ただし、各機能ガイドが他の機能ガイドビューの競合を考慮しない場合、複数のガイドが同時に表示されるため、ユーザーエクスペリエンスに大きな影響を与え、ガイド効果が低下します。したがって、ビューを整然とした管理機能でガイドすることが非常に重要です。

まず、私たち自身のビジネスシナリオに従って、さまざまな種類のガイダンスを整理する必要があります。各ブートストラップを正確に区別するには、列挙定義を使用します。

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コード行により、押された状態がビューに追加されます


Android開発では、UEが押された状態の効果を追加するように要求することがよくあります。従来の記述方法は、セレクターを使用して、押された状態とデフォルトの状態のリソースをそれぞれ設定することです。コード例は次のとおりです。

<?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
    }
}
复制代码

用户对屏幕的操作,可以简单划分为以下几个最基础的事件:

image.png

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

image.png

以上三个方法均有一个布尔类型的返回值,通过返回 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官方提供的一个解决方案,能够比较优雅地解决这个问题,如下描述:

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的点击区域的目的。

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にはAndroid 8.0以前のバグがあります。下位バージョンとの互換性が必要な場合は、注意が必要です。デリゲートを介して子ビューのクリックイベントをトリガーした後、親ビュー自体がクリックイベントを監視します。 TouchDelegateクリックイベント転送(onTouchEvent)の処理で、MotionEvent.ACTION_DOWNに問題があります。クリック範囲内にない場合、mDelegateTargeted変数はfalseにリセットされないため、親ビューはクリックイベントを受信できなくなり、クリックなどの操作を処理できなくなります。関連する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;
……
复制代码

推奨読書[テクニカルガソリンスタンド]シリーズ:

人工知能の超大規模な事前トレーニングモデルの簡単な紹介

自動テスト生成の分野におけるBaiduインテリジェントテストの調査の謎を解き明かす

小プログラム自動テストフレームワークの原理の分析

おすすめ

転載: juejin.im/post/7099010216855470087