小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
按下效果器
Android在5.0以上 Button默认自带阴影效果,这是为了增加立体性和视觉效果,是非常好的。但是TMD,产品一句话: "跟IOS一样!",苦逼的Android开发就要去掉了,我们可以在xml中添加如下代码去掉:
style="?android:attr/borderlessButtonStyle"
复制代码
这样还不行,因为IOS的Button还有个按下的透明度效果,那么我们就需要再加上透明度效果。
我们先实现个简单的透明度工具类:
public class PressEffect {
public static void TouchEffect(View view, int action) {
if (view == null) return;
if (!view.isClickable()) return;
if (MotionEvent.ACTION_DOWN == action) {
// 接近0.618趋紧完美
view.setAlpha(0.6f);
} else {
view.setAlpha(1.0f);
}
}
}
复制代码
然后,我们自定义一个Button,在onTouch里面调用即可:
public class CButton extends Button {
@Override
public boolean onTouchEvent(MotionEvent event) {
PressEffect.TouchEffect(this, event.getAction());
return super.onTouchEvent(event);
}
}
复制代码
当然,不仅仅是Button,你可以自定义任意View,只要复写其onTouch()事件,然后设置clickable属性为true,就可以生效。
有人说,这还要自定义View,太费劲了,其实这是不对的,我们的项目如果要(或者将来可能)对app的所有界面进行整体处理,怎么办?如果你用了自定义View,只需要将所有View进行横切处理即可,如果没有,那你可能就很费劲了。所以我们要有面向未来的思想。
比如下面的App黑白化处理。
黑白模式
先上代码,下面代码将View设置为黑白模式:
public static void darkTheme(View view) {
if (view == null) return;
Paint paint = new Paint();
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
}
复制代码
这是通过设置饱和度实现的。

下面代码则将黑白模式还原:
public static void resetTheme(View view) {
if (view == null) return;
view.setLayerType(View.LAYER_TYPE_HARDWARE, new Paint());
}
复制代码
效果如下(这里传递了Activity的DecorView):
我们看到,点击Dark就变成黑白模式,点击Reset就还原了。而且,Dark按钮按下时是没有阴影的,而是变为透明的,抬起则还原;点击Reset是有阴影的。这是因为Dark使用了上面的定义的CButton,PressEffect生效了。
那么,如果整个App全部需要黑白化处理呢,这里就有两种方案:
- 1 遍历每一个Activity/Dialog然后获取到DecorView来进行设置。
- 2 定义一套完整的自定义View,然后在这些View里面处理。
方案2是比较灵活的,对业务无侵入性。你可以自己定义一套TextView、Button、LinearLayout等,继承自系统自带的,后续如果有更改,可以统一在这些自定义View里面处理(因为系统自带的View你没法改)。
Switch的坑
我们知道,对于Switch来说,如果设置了OnCheckedChangeListener,那么调用switch.setChecked(true)就会触发事件里面的回调,导致走一些非必要的逻辑,这是不对的。
说白了,我们希望的是: 如果是checked属性是通过代码设置的,就不触发;如果是用户点击导致的,就触发,所以我们可以通过是否按下来判断:
switch.setOnCheckedChangeListener{ buttonView, isChecked ->
// 如果没有按下,则认为是代码设置的,直接拦截
if(!buttonView.isPressed()){
return;
}
// ...其他业务
}
复制代码
这里就不再展示效果了。
连点拦截器
在我们的业务中,有很多因为用户频繁点击导致的问题,比如点击某个按钮去刷新数据,如果频繁点击就会请求好多次接口,不处理的话单身30年手速的人可能会直接干死一台服务器。
所以我们需要添加保护,避免频繁点击的情况发生,我们可以给点击事件添加一个间隔,小于这个间隔就不触发点击事件,如下:
public abstract class ClickProtector implements View.OnClickListener {
// 点击时间间隔: ms
private long delay = 0;
// 实际的点击事件
abstract void onRealClick(View v);
/**
* 设置点击间隔
*
* @param delay 单位:ms
*/
public ClickProtector delay(long delay) {
this.delay = delay;
return this;
}
@Override
public void onClick(View v) {
int key = v.hashCode();
Long lastTime = (Long) v.getTag(key);
// 没点击过 或者 本次点击跟上次点击时间差小于delay,就拦截
if (lastTime != null && System.currentTimeMillis() - lastTime < delay) {
return;
}
// 触发点击事件
onRealClick(v);
// 记录本次点击时间
v.setTag(key, System.currentTimeMillis());
}
}
复制代码
效果如下:
其中只有Add设置了拦截器如下:
findViewById<Button>(R.id.btn_dark).setOnClickListener(object : ClickProtector() {
override fun onRealClick(v: View?) {
tvCount.text = "${++number}"
}
}.delay(1000))
复制代码
我们设置了delay为1000,也就是1s内触发一次。
当然,你也可以改进这个点击保护器,比如添加"拦截时候的回调",可以给用户提示信息等。如下:
// 拦截的回调
private List<Runnable> runnables = new ArrayList<>();
// 没点击过 或者 本次点击跟上次点击时间差小于delay,就拦截
if (lastTime != null && System.currentTimeMillis() - lastTime < delay) {
// 执行拦截的回调
for (Runnable runnable : runnables) {
runnable.run();
}
return;
}
复制代码
也可以扩展其他拦截器。