Пользователь
0,0
рейтинг
11 января в 09:30

Разработка → Custom rounded view из песочницы

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
Pavel Dolbik @pavel_dolbik
карма
6,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

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

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

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

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


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

  • +1
    Неплохо бы onMeasure() поправить, чтобы избежать
    вот таких вещей

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

    1) canvas.drawBitmap(finalBitmap, rect, rect, imgPaint); rect здесь не имеет никакого смысла. потому что суть метода в том, чтобы эти src и dst прямоугольники были разные, чтобы при отрисовки применился скейл и сдвиг. можно было написать чуть проще canvas.drawBitmap(finalBitmap, 0, 0, imgPaint), где 0 это верхняя и левая позиция для отрисовки. или же можно было использовать этот rect для скейла исходной картинки и обойтись без шага Bitmap finalBitmap = bitmap.createScaledBitmap(bitmap, radius, radius, false); и необходимости создавать промежуточный Bitmap (кстати, у вас параметр метода называется radius, хотя используете вы его как диаметр):
        private Bitmap getRoundedCroppedBitmap(Bitmap bitmap, int size) {
            Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
            Rect rect = new Rect(0, 0, size, size);
    
            Canvas canvas = new Canvas(output);
            canvas.drawCircle(
                    size  / 2,
                    size / 2,
                    size / 2,
                    imgPaint);
    
            imgPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
            canvas.drawBitmap(finalBitmap, null, rect, imgPaint);
    
            return output;
        }
    

    2) в onTouchEvent следует еще реагировать на MotionEvent.ACTION_CANCEL — он вызовется когда юзер нажмет пальцем в области вашей View, но потом уведет палец куда то в сторону, то есть фактически ACTION_UP будет выполнен потом уже не в области вашей View. На этот event не стоит реагировать как на tap, то есть отмечать картинку как выделенную, но стоит выполнить обратную scale анимацию, иначе вьюшка так и останется уменьшенной.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.