View的几个小工具

4,030 阅读4分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

按下效果器

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_add).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;
}

也可以扩展其他拦截器。