image

Введение


В наше время существует много пользователей, на мобильных устройствах которых установлено приложение Instagram. В данном приложении существует замечательная функция – поделится понравившимся изображением. И при выборе пользователя, которому мы желаем отправить понравившейся пост, мы видим скругленный аватар, при клике на который отображается анимация и осуществляется выделение. Реализацию данного элемента мне захотелось повторить.

Начальный этап

Создадим новый класс, наследник android.widget.ImageView.

public class MyView extends ImageView {

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

Создадим метод init() в котором инициализируем объекты Paint для рисования графических объектов.

public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        redBorder = new Paint();
        redBorder.setAntiAlias(true);
        redBorder.setColor(Color.RED);
        redBorder.setStrokeWidth(redStrokeWidth);
        redBorder.setStyle(Paint.Style.STROKE);

        imgPaint = new Paint();
        imgPaint.setAntiAlias(true);
        imgPaint.setFilterBitmap(true);
        imgPaint.setDither(true);
    }

Объект Paint redBoard отвечает за отрисовку красной рамки, которая будет отображаться при выделении. Флаг setAntiAlias(true) обеспечивает сглаживание. Затем устанавливаем красный цвет (setColor(Color.RED)), ширину контура setStrokeWidth(redStrokeWidth), redStrokeWidth обычная переменная (private int redStrokeWidth = 10;) и устанавливаем стиль отрисовки setStyle(Paint.Style.STROKE) — рисовать очертания графического примитива.

Объект Paint imgPaint отвечает за скругление аватара пользователя. Флаг setFilterBitmap(true) гласит о том, что фильтрация будет оказывать влияние на растровое изображение при трансформации. Флаг setDither(true) используется для сглаживания цветов, позволяет уменьшить визуальные артефакты.

Преобразование изображения

Получаем изображение над которым будем делать все необходимые преобразования.

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        decrement = (w * decrementFactor)/100;
        bitmapPosition = decrement /2;

        Drawable drawable = getDrawable();
        if (drawable != null) {
            Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
            roundBitmap = getRoundedCroppedBitmap(bitmap, getWidth() - decrement);
        } else {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
            roundBitmap = getRoundedCroppedBitmap(bitmap, getWidth() - decrement);
        }
    }

Т.к. при выделении нужно отрисовать красную рамку и отступ от изображения, вводим переменную decrementFactor которая, как в данном случаи, будет составлять 15% от всего размера элемента (private int decrementFactor = 15;). Т.е. сам элемент имеет размер w, а изображение внутри него будет на 15% меньше. Так же вычисляем значение для переменной bitmapPosition, которая будет использоваться при позиционировании изображения. Осуществляем проверку на наличие изображения. Если изображение установлено, делаем преобразования над ним (метод getRoundedCroppedBitmap()) иначе, берем логотип.

Метод getRoundedCroppedBitmap() отвечает за скругление изображения.

private Bitmap getRoundedCroppedBitmap(Bitmap bitmap, int radius) {
        Bitmap finalBitmap = bitmap.createScaledBitmap(bitmap, radius , radius, false);
        Bitmap output = Bitmap.createBitmap(finalBitmap.getWidth(), finalBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Rect rect = new Rect(0, 0, output.getWidth(), output.getHeight());

        Canvas canvas = new Canvas(output);
        canvas.drawCircle(
                finalBitmap.getWidth()  / 2,
                finalBitmap.getHeight() / 2,
                finalBitmap.getWidth() / 2,
                imgPaint);

        imgPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(finalBitmap, rect, rect, imgPaint);

        return output;
    }

С помощью метода createScaledBitmap() изменяем размер изображения, на основании смаштабированного изображения формируем новое, которое и будет результатом работы метода. Создаем объект Rect rect, на основании которого будем рисовать bitmap. Получаем canvas, русуем круг и для объекта Paint изменяем режим Xfermode который влияет на способ наложения новых цветов поверх уже нарисованных.

Т.е. мы изменили размер изображения на тот, который нам нужен. Нарисовали над этим изображением круг и осуществили наложение круга на изображение, сказав, что будет видно то, что входит в круг.

Нарисуем полученный элемент:

@Override
 protected void onDraw(Canvas canvas) {
     canvas.drawBitmap(roundBitmap, bitmapPosition, bitmapPosition, null);
     if (imgSelected) {
         canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - redStrokeWidth, redBorder);
     }
 }

Для того, что бы наш элемент начал реагировать на нажатия, переопределим метод:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Animation scale = AnimationUtils.loadAnimation(getContext(), R.anim.scale);
                startAnimation(scale);
                break;
            case MotionEvent.ACTION_UP:
                imgSelected = !imgSelected;
                invalidate();
                Animation scale2 = AnimationUtils.loadAnimation(getContext(), R.anim.scale_2);
                startAnimation(scale2);
                break;
        }
        return true;
    }

При нажатии будем немного уменьшать элемент. Загружаем xml-файл в котором описана анимация для уменьшения:

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="1.0"
    android:toXScale="0.9"
    android:fromYScale="1.0"
    android:toYScale="0.9"
    android:duration="200"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fillAfter="true">
</scale>

А когда пользователь отпустит палец, нарисуем красную рамку и немного увеличим элемент:

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="1.0"
    android:toXScale="1.1"
    android:fromYScale="1.0"
    android:toYScale="1.1"
    android:duration="200"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fillAfter="true">
</scale>

Заключение

В результате получился милый элемент с простой анимацией.

Код проекта доступен на git.

Буду рад комментариям. Спасибо.

image

Комментарии (8)


  1. Bringoff
    11.01.2016 10:33

    Это не статья, а листинг с комментариями.


    1. lazexe
      11.01.2016 17:10

      Обратите внимание, что это «из песочницы». Зачем сразу давить человека негативными коментарием, это же его первая статья на хабре. После похожих коментариев желание писать другую статью напрочь отпадает, из-за опасения получать похожие коментарии. Можно было бы и более снисходительно.

      Автору: молодец, есть некоторые косяки в реализации (см. комментарий ниже), но в целом неплохо как для начала!


      1. Bringoff
        11.01.2016 17:18
        +1

        это же его первая статья на хабре

        Это отлично, что сюда еще кто-то пишет, но раз написал, то будь готов воспринимать объективную критику. Это реально код с комментариями, за ним я могу сходить на гитхаб.
        желание писать другую статью напрочь отпадает, из-за опасения получать похожие коментарии

        Если и следующая статья будет такой, то не страшно, если автор ее не напишет :) Для себя такие велосипеды писать можно и нужно, а на хабре хочется интересных и полезных статей, которых последнее время и так мало.
        Да и вообще взрослого человека мало должны волновать комментарии в этих ваших интернетах. Будет, что сказать, — напишет, не переживайте.


        1. lazexe
          11.01.2016 17:22

          Ах да, и надо обязательно минусовать, спасибо что напомнили.


          1. Bringoff
            11.01.2016 17:24

            Начнем с того, что это не я.

            Скрытый текст


            Вы же вроде уже взрослый, а на минусы реагируете, как я в 16 :)


  1. rogrom
    11.01.2016 16:06
    +1

    Неплохо бы onMeasure() поправить, чтобы избежать

    вот таких вещей


  1. DnV
    11.01.2016 20:52

    Такое интересно самому писать, а не читать. Или уж просто юзать не вдаваясь в подробности. Кстати, делал похожее под iOS, кому интересно: https://github.com/DnV1eX/DNVAvatar
    pavel_dolbik, а почему у вас в статье и на гитхабе все картинки сплющены?


  1. Krizai
    12.01.2016 09:57

    Чем лучше делать кастомную отрисовку, чем просто наложить поверх еще один ImageView с рамкой?