Android 按鈕 pressed 狀態(tài)的顯示時(shí)機(jī) (附少許源碼分析)

根據(jù)評論反饋,整理成了一個(gè)系列,三種解決方案,文章3 應(yīng)該是三個(gè)中最合理的方案握恳。這三篇依次看下來,可以看到解決一個(gè)問題走過的彎路:

  1. Android 運(yùn)行時(shí)給動(dòng)態(tài)加載的圖標(biāo)按鈕添加點(diǎn)擊效果
  2. 本文
  3. Android Drawable / DrawableCompat # setTintList( ) 使用時(shí)一個(gè)值得注意的問題

本文是接著上一篇文章 文章1 捺僻,繼續(xù)聊聊關(guān)于按鈕的按下狀態(tài)的問題乡洼。

上一篇文章中,關(guān)于不規(guī)則形狀圖片按鈕在運(yùn)行時(shí)設(shè)置pressed狀態(tài)的問題匕坯,提出了一種思路:用 PorterDuff.Mode 的 SRC_IN 模式裁剪一張半透明灰度蒙層圖來生成與按鈕的normal狀態(tài)圖形狀一致的蒙層束昵,然后,再疊加兩張圖生成pressed狀態(tài)下的圖醒颖,最后組合成 StateListDrawable 來解決 pressed state 的問題妻怎。

文章發(fā)出后,評論區(qū)李 @我是李小平 提示說這種方式“曲線救國”泞歉,并提示了可以用著色的方式來簡單實(shí)現(xiàn)需求逼侦,具體可以見他的評論匿辩。其實(shí)道理很簡單,就是用ColorFilter對圖片進(jìn)行著色榛丢,產(chǎn)生pressed的效果铲球。但是著色的方式,相對于StateListDrawable(或selector)的方式晰赞,需要自己監(jiān)聽touch事件并判斷動(dòng)作類型來進(jìn)行著色操作稼病。感謝 @我是李小平 的提示,我具體實(shí)現(xiàn)了一下掖鱼,把實(shí)現(xiàn)中遇到的著色時(shí)機(jī)的細(xì)節(jié)問題然走,在這篇文章中記錄一下。

什么時(shí)候顯示pressed狀態(tài)戏挡?

由于我們的頁面是一個(gè)可以滑動(dòng)并且可以下拉刷新的頁面芍瑞,因此,其頁面上的按鈕在處理touch事件時(shí)就需要考慮區(qū)分點(diǎn)擊事件和滑動(dòng)事件褐墅。具體到這里說的按鈕的點(diǎn)擊狀態(tài)的問題拆檬,我們給按鈕設(shè)置了OnTouchListener,肯定不能在收到ACTION_DOWN事件后立刻就設(shè)置著色(即設(shè)置為pressed狀態(tài))妥凳,因?yàn)榇藭r(shí)用戶手剛接觸屏幕竟贯,接下來可能是短點(diǎn)擊,也可能是滑動(dòng)逝钥,所以需要有一個(gè)延時(shí)來判斷具體是哪種動(dòng)作屑那,這個(gè)延時(shí)的時(shí)長,系統(tǒng)有一個(gè)特定的值晌缘,可以通過 ViewConfiguration 獲取齐莲。

ViewConfiguration.java 源碼截圖

跟進(jìn)查看 TAP_TIMEOUT 這個(gè)值是100(毫秒),注釋說明磷箕,如果在這個(gè)時(shí)長之內(nèi)用戶沒有移動(dòng),就判定為一次點(diǎn)擊阵难,否則判定為scroll岳枷。

所以,在我們的實(shí)現(xiàn)中呜叫,也應(yīng)該用這個(gè)值空繁。在OnTouchListener的onTouch( )方法中:

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        downX = event.getX();
        downY = event.getY();

        // 發(fā)送延時(shí)消息装哆,進(jìn)行著色(即設(shè)置為pressed狀態(tài))
        handler.sendEmptyMessageDelayed(MSG_TINT, ViewConfiguration.getTapTimeout());
        break;

        ...
}

如果用戶在這個(gè)時(shí)間段內(nèi)有移動(dòng)乒省,則要取消這個(gè)消息。如何判定為移動(dòng)夷蚊,ViewConfiguration也有個(gè)可以獲取 ** touchSlop ** 值的方法娱颊,大于這個(gè)touchSlop的傲诵,則判定為滑動(dòng):

ViewConfiguration.java 源碼截圖

所以凯砍,在收到ACTION_MOVE事件時(shí),按這個(gè)標(biāo)準(zhǔn)判定是否移動(dòng):

...

case MotionEvent.ACTION_MOVE:
    float dx = event.getX() - downX;
    float dy = event.getY() - downY;
    if (touchSlop == 0) {
        touchSlop = ViewConfiguration.get(target.getContext()).getScaledTouchSlop();
    }

    // 如果判定為移動(dòng)拴竹,在handler中remove掉進(jìn)行著色的消息
    if ((dx * dx) + (dy * dy) > (touchSlop * touchSlop) ) {
        handler.removeMessages(MSG_TINT);
    }
    break;

case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
    // 動(dòng)作結(jié)束時(shí)悟衩,清除著色,按鈕由pressed狀態(tài)恢復(fù)為normal狀態(tài)
    clearTint();
    handler.removeMessages(MSG_TINT);
    break;

...

還有一個(gè)小問題

實(shí)現(xiàn)到這一步栓拜,還有個(gè)問題:當(dāng)點(diǎn)擊稍微快一些的時(shí)候座泳,經(jīng)常是看不到按鈕的pressed狀態(tài),即著色后的效果的幕与。原因稍一想也很明顯:前面講到挑势,為了區(qū)分滑動(dòng)和點(diǎn)擊,我們并沒有在ACTION_DOWN的時(shí)候立刻著色啦鸣,而是有一個(gè)延時(shí)潮饱,那么如果點(diǎn)擊的時(shí)候從ACTION_DOWN到ACTION_UP的時(shí)間小于這個(gè)延時(shí),就沒有觸發(fā)著色赏陵。

怎么解決饼齿?測試發(fā)現(xiàn)用StateListDrawable(即selector)的方式是沒問題的,點(diǎn)擊再快也有pressed效果蝙搔。而且既然這個(gè)時(shí)延是從系統(tǒng)獲得缕溉,那么我們不妨看看源碼中是怎么解決這個(gè)問題的。開始我猜測源碼應(yīng)該是在ACTION_UP時(shí)候做了一次置為pressed狀態(tài)的動(dòng)作吃型,然后一定短時(shí)間后再取消狀態(tài)证鸥。這樣視覺上可以達(dá)到效果。StateListDrawable(即selector)的方式勤晚,依賴于把View setPressed(true)枉层, 那我們就搜索這個(gè)方法的調(diào)用。查看TextView源碼未發(fā)現(xiàn)赐写,再到View源碼鸟蜡,發(fā)現(xiàn)果然是這樣的:

View.java 源碼截圖

注釋說的很清楚:*** 按鈕在還沒來得及顯示為pressed狀態(tài)之前就被release了,那么就現(xiàn)在(在觸發(fā)click之前)再來顯示為pressed狀態(tài)挺邀,確保用戶看到效果揉忘。***

再往下一點(diǎn)點(diǎn),還是在這個(gè)ACTION_UP的處理中端铛,有取消pressed狀態(tài)的代碼:

View.java 源碼截圖

其中 ViewConfiguration.getPressedStateDuration() 就是表示應(yīng)該顯示多久的pressed狀態(tài)泣矛。

這樣就全部明朗了,我們也按照這種方式處理即可禾蚕。文章最后貼上完整代碼您朽。

扯遠(yuǎn)一點(diǎn)

注意上面View源碼片段中的 if (prepressed),這個(gè)prepressed 局部變量是怎么回事换淆。我們看它的賦值:

 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; ``` 

可以猜測這個(gè)prepressed就表示上面提到的沒來的及顯示pressed狀態(tài)就ACTION_UP了哗总,需要“補(bǔ)救”几颜。在搜索 ``` PFLAG_PREPRESSED ``` 這個(gè)常量的用處,發(fā)現(xiàn)只有一個(gè)地方它被賦給了 ``` mPrivateFlags ``` :

![View.java 源碼截圖](http://upload-images.jianshu.io/upload_images/71249-cdc3970f3c27fb55.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可以看到魂奥,這段代碼就是說在ACTION_DOWN的時(shí)候菠剩,判斷一下,當(dāng)前控件如果是一個(gè)可滾動(dòng)布局的子View耻煤,那么就延遲設(shè)置pressed狀態(tài)具壮;否則直接設(shè)置 ``` setPressed(true, x, y) ``` 。

看一下 ``` isInScrollingContainer ``` :

![View.java 源碼截圖](http://upload-images.jianshu.io/upload_images/71249-44c3d3ef5e2bb236.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

就是沿著View的層級結(jié)構(gòu)一層層往上找哈蝇,如果有一層的父布局是可滾動(dòng)的棺妓,那么就return true;如果所有層級的父布局都不可滾動(dòng)炮赦,才return false怜跑。

再看看 ``` ViewGroup ``` 中 ``` shouldDelayChildPressedState( ) ``` 這個(gè)方法在各個(gè)子類的覆寫。發(fā)現(xiàn)像 ``` LinearLayout ``` 
吠勘、``` FrameLayout ``` 這樣的不可滾動(dòng)的Layout性芬,返回 false;而 ``` ScrollView ``` 這樣可滾動(dòng)的則返回 true剧防。

而其實(shí)在我的需求中植锉,我們的頁面是已知可以滾動(dòng)的,所以峭拘,如果只考慮在這種場景下使用的話俊庇,可以省略這一判斷。

下面是我寫的一個(gè)PressTintedDrawableViewWrapper類鸡挠,可以支持為ImageView或TextView里的Drawable設(shè)置pressed效果辉饱,我們的按鈕是用TextView的CompoundDrawable實(shí)現(xiàn)的,因此用法如下:

```java
new PressTintedDrawableViewWrapper(Color.parseColor("#4C333333")).wrap(someTextView).apply();

附上 PressTintedDrawableViewWrapper 類完整代碼:

public class PressTintedDrawableViewWrapper implements View.OnTouchListener {

    private static final int MSG_TINT = 1;
    private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();

    private View target;
    private Drawable[] drawables;
    private int tintColor;

    private Handler handler;
    private float downX, downY;
    private int touchSlop;

    private boolean tinted = false;

    public PressTintedDrawableViewWrapper(int tintColor) {
        this.tintColor = tintColor;
    }

    public PressTintedDrawableViewWrapper wrap(TextView textView) {
        this.target = textView;
        this.drawables = textView.getCompoundDrawables();
        return this;
    }

    public PressTintedDrawableViewWrapper wrap(ImageView imageView) {
        this.target = imageView;
        this.drawables = new Drawable[]{imageView.getDrawable()};
        return this;
    }

    public boolean apply() {
        if (drawables != null && drawables.length > 0) {
            handler = new TouchHandler(this);
            target.setOnTouchListener(this);

            return true;
        } else {
            return false;
        }
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();

                handler.sendEmptyMessageDelayed(MSG_TINT, TAP_TIMEOUT);
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = event.getX() - downX;
                float dy = event.getY() - downY;
                if (touchSlop == 0) {
                    touchSlop = ViewConfiguration.get(target.getContext()).getScaledTouchSlop();
                }
                if ((dx * dx) + (dy * dy) > (touchSlop * touchSlop) ) {
                    handler.removeMessages(MSG_TINT);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!tinted) {
                    if (handler.hasMessages(MSG_TINT)) {
                        handler.removeMessages(MSG_TINT);
                        applyTint();
                        target.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                clearTint();
                            }
                        }, ViewConfiguration.getPressedStateDuration());
                    }
                } else {
                    clearTint();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                clearTint();
                handler.removeMessages(MSG_TINT);
                break;
        }

        return false;
    }

    private void applyTint() {
        ColorFilter colorFilter = new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP);
        for (Drawable drawable : drawables) {
            if (drawable != null) {
                drawable.mutate().setColorFilter(colorFilter);
            }
        }
        tinted = true;
    }

    private void clearTint() {
        if (tinted) {
            for (Drawable drawable : drawables) {
                if (drawable != null) {
                    drawable.mutate().clearColorFilter();
                }
            }
            tinted = false;
        }
    }

    private static class TouchHandler extends Handler {
        WeakReference<PressTintedDrawableViewWrapper> ref;

        public TouchHandler(PressTintedDrawableViewWrapper view) {
            this.ref = new WeakReference<>(view);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_TINT:
                    PressTintedDrawableViewWrapper view = ref.get();
                    if (view != null) {
                        view.applyTint();
                    }
                    break;
            }
        }
    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拣展,一起剝皮案震驚了整個(gè)濱河市彭沼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌备埃,老刑警劉巖溜腐,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瓜喇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)歉糜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門乘寒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人匪补,你說我怎么就攤上這事伞辛±煤玻” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵蚤氏,是天一觀的道長甘耿。 經(jīng)常有香客問我,道長竿滨,這世上最難降的妖魔是什么佳恬? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮于游,結(jié)果婚禮上毁葱,老公的妹妹穿的比我還像新娘。我一直安慰自己贰剥,他們只是感情好倾剿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚌成,像睡著了一般前痘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上担忧,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天芹缔,我揣著相機(jī)與錄音,去河邊找鬼涵妥。 笑死乖菱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蓬网。 我是一名探鬼主播窒所,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼帆锋!你這毒婦竟也來了吵取?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锯厢,失蹤者是張志新(化名)和其女友劉穎皮官,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體实辑,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捺氢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了剪撬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摄乒。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馍佑,到底是詐尸還是另有隱情斋否,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布拭荤,位于F島的核電站茵臭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏舅世。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一歇终、第九天 我趴在偏房一處隱蔽的房頂上張望社证。 院中可真熱鬧,春花似錦评凝、人聲如沸追葡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宜肉。三九已至,卻和暖如春翎碑,著一層夾襖步出監(jiān)牢的瞬間谬返,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工日杈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遣铝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓莉擒,卻偏偏與公主長得像酿炸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子涨冀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容