Monday, October 24, 2011

Затемнение ImageView по нажатию

Недавно разобрался с такими типами Drawable, как LayerDrawable и StateListDrawable.
Для начала опишу задачу. Есть GridView, каждая ячейка которого представляет собой картинку 80x80. Картинка получается с сервера. Не так давно заказчик захотел, чтобы ячейка представляла собой не просто картинку, а картинку + круговую тень поверх нее, примерно вот такую, чтобы приложение смотрелось немного "гламурнее":
Кроме того, при нажатии на картинку картинка должна была еще затемняться одноцветной черной полупрозрачной маской.
Как это реализовать?
Вручную программно выполнять какие-то наложения картинок не захотелось сразу. Почти сразу решил сделать 3 картинки во FrameLayout, лежащих одна на другой, но после небольшой попытки выполнять соответствующий setVisibility на полупрозрачную маску по событию MotionEvent от этого тоже захотелось отказаться.
Вспомнились селекторы, которые позволяют указать соответствующий drawable элементу в зависимости от своего state - pressed, focused, и др. Но на свойство visibility нельзя повесить никакой selector, что, в принципе, логично. Поиск привел меня к LayerDrawable, которые позволяют создавать один Drawable по слоям из нескольких. Идея свелась к тому, чтобы использовать selector, в котором на pressed state подключать LayerDrawable с тремя вложенными картинками, на обычный state - с двумя.
Однако - сами-то картинки (не декоративные, а информационные) каждый раз разные! Поэтому на чистом xml, при всем желании, реализовать бы все не удалось, поэтому я плюнул, и решил все реализовать программно с использованием уже знакомых LayerDrawable и StateListDrawable (который может реализовать selector прямо в коде).
Получившийся код:
public class ObscuredImageView extends ImageView {
    protected static final String TAG = "ObscuredImageView";
    private Drawable _innerShading;
    private Drawable _obscured;

    public ObscuredImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        _innerShading = getContext().getResources().getDrawable(R.drawable.inner_shading);
        _obscured = getContext().getResources().getDrawable(R.color.semitransparent);
    }

    @Override
    public void setImageBitmap(Bitmap bitmap) {
        Log.v(TAG, "Going to set state drawable to ImageView");
        BitmapDrawable image = new BitmapDrawable(getContext().getResources(), bitmap);
        Drawable pressed = new LayerDrawable(new Drawable[] { image, _innerShading, _obscured });
        Drawable normal = new LayerDrawable(new Drawable[] { image, _innerShading });
        StateListDrawable states = new StateListDrawable();
        states.addState(new int[] { android.R.attr.state_pressed }, pressed);
        states.addState(new int[] { android.R.attr.state_focused }, pressed);
        states.addState(new int[] {}, normal);
        setImageDrawable(states);
    }
}

Решение вызывать setImageDrawable в переопределенном методе setImageBitmap на первый взгляд выглядит неудачно и "грязно", но если мы заглянем в исходный код класса ImageView, то мы увидим следующее:
public void setImageBitmap(Bitmap bm) {
    // if this is used frequently, may handle bitmaps explicitly
    // to reduce the intermediate drawable object
    setImageDrawable(new BitmapDrawable(bm));
}
Так что вызов setImageDrawable абсолютно корректен.

Таким образом, я получил работающее и гибкое решение. Если кто-то реализовал подобное другим образом - с интересом приму к сведению.

1 comment:

  1. намного проще


    public class ObscuredImageView extends ImageView {

    private boolean mIsSelected;

    public ObscuredImageView(Context context) {
    super(context);
    init();
    this.setClickable(true);
    }

    public ObscuredImageView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
    this.setClickable(true);
    }

    public ObscuredImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
    this.setClickable(true);
    }

    private void init() {
    mIsSelected = false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN && !mIsSelected) {
    setColorFilter(0x99000000);
    mIsSelected = true;
    } else if (event.getAction() == MotionEvent.ACTION_UP && mIsSelected) {
    clearColorFilter();
    mIsSelected = false;
    }

    return super.onTouchEvent(event);
    }

    }

    ReplyDelete