根據(jù)評論反饋,整理成了一個(gè)系列,三種解決方案,文章3 應(yīng)該是三個(gè)中最合理的方案握恳。這三篇依次看下來,可以看到解決一個(gè)問題走過的彎路:
- Android 運(yùn)行時(shí)給動(dòng)態(tài)加載的圖標(biāo)按鈕添加點(diǎn)擊效果
- 本文
- 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 獲取齐莲。
跟進(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):
所以凯砍,在收到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)果然是這樣的:
注釋說的很清楚:*** 按鈕在還沒來得及顯示為pressed狀態(tài)之前就被release了,那么就現(xiàn)在(在觸發(fā)click之前)再來顯示為pressed狀態(tài)挺邀,確保用戶看到效果揉忘。***
再往下一點(diǎn)點(diǎn),還是在這個(gè)ACTION_UP的處理中端铛,有取消pressed狀態(tài)的代碼:
其中 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;
}
}
}
}