В этом уроке:

- используем метод Canvas.saveLayer

В Уроке 146 мы разбирались с методом save. С его помощью можно сохранить состояние канвы, выполнить различные преобразования и вернуться к сохраненному состоянию методом restore. Метод saveLayer создает отдельный от канвы Bitmap и переадресует ему все последующие операции канвы. А чтобы потом записать получившийся результат с Bitmap-а на канву, необходимо вызвать метод restore. Те, кто хотя бы минимально работал с графическими редакторами, могут провести аналогию со слоями. Вы создаете отдельный слой, рисуете на нем что-либо, затем сливаете его с основным изображением. Собственно, метод так и называется saveLayer – «сохранить слой».

Поначалу кажется, что этот метод – абсолютно бессмысленный. Какой смысл выделять отдельный слой, чтобы нарисовать на нем что-то и потом все равно вывести это на основной канве? Проще сразу на канве и рисовать. Оказывается, смысл есть. И сейчас мы разберем пример, после которого станет понятно, зачем может понадобиться этот механизм.

Возьмем картинку

 

и попробуем нарисовать такой эффект.

Т.е. на картинку наложена рамка. В центре рамка прозрачная, а к краям становится затемненной полупрозрачной.

Идею для примера я любезно спер отсюда. Но чтобы уж совсем не палиться, картинку взял другую.

 

План такой:

1) Выводим на канву картинку

2) Создаем слой, на котором нарисуем полупрозрачную рамку

3) Накладываем слой-рамку на картинку

 

Начнем с создания полупрозрачной рамки. 

Класс MainActivity:

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(new DrawView(this));
  }

  class DrawView extends View {

    Paint mShaderPaint;
    Paint mBlackPaint;
    Paint mPaint;
    Bitmap mBitmap;
    Rect mRect = new Rect(0, 40, 750, 370);
    RectF mRectF = new RectF(mRect);

    public DrawView(Context context) {
      super(context);
      setLayerType(LAYER_TYPE_SOFTWARE, null);
      init();
    }

    private void init() {
      mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mShaderPaint.setShader(createShader());
    }

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawOval(mRectF, mShaderPaint);
    }

    private Shader createShader() {
      final int[] colors = new int[] { 0xff000000, 0xff000000, 0 };
      final float[] anchors = new float[] { 0, 0.5f, 1 };
      Shader shader = new android.graphics.RadialGradient(0, 0, 1,
          colors, anchors, Shader.TileMode.CLAMP);

      Matrix matrix = new Matrix();
      matrix.postTranslate(mRect.centerX(), mRect.centerY());
      matrix.postScale(mRect.width() / 2, mRect.height() / 2,
          mRect.centerX(), mRect.centerY());

      shader.setLocalMatrix(matrix);
      return shader;
    }

  }

}

 В методе init создаем кисть и устанавливаем ей шейдер, созданный в методе createShader.

Для создания шейдера в createShader используем градиент-шейдер (Урок 165). Он черный (ff000000) в центре и будет становиться прозрачным (00000000) к краям. Обратите внимание, что мы создали его в точке (0,0) и радиусом он всего 1. Далее мы применяем к нему матрицу (Урок 144), чтобы поместить его в нужную точку и придать ему необходимые размеры.

mRect – это координаты прямоугольника, в котором будет выведена картинка. Соответственно центр градиента нам надо поместить в центр mRect, а размер градиента должен быть равен размеру mRect.

В методе onDraw нарисуем на экране овал, используя созданный шейдер.

Центр градиента находится в центре mRect-прямоугольника. А форма градиента, изначально круглая, немного сжата по вертикали, чтобы влезть в прямоугольник. Это результат работы матрицы.

 

Этот градиент мы сейчас будем использовать для создания необходимой нам рамки. Для этого мы возьмем полупрозрачный черный фон

и сверху нарисуем на нем градиент в режиме PorterDuff.Mode.DST_OUT (Урок 154).

Смотрим формулу расчета итоговых альфы и цвета для режима DST_OUT: [Da * (1 - Sa), Dc * (1 - Sa)].

В нашем случае:
Da - уровень прозрачности черного фона
Dc - цвет черного фона
Sa - уровень прозрачности градиента

Заметьте, что от градиента в формуле используется только альфа. Т.е. цвет там может быть хоть красно-зеленый. Он будет проигнорирован этим режимом наложения.

Т.е. там, где градиент наиболее непрозрачен, выражение (1 - Sa) стремится к нулю, а следовательно и стремятся к нулю итоговые значения альфы и цвета, полученные в результате наложения. И пикселы там будут максимально прозрачные.

А там, где градиент наименее прозрачен, выражение (1 - Sa) стремится к единице, а следовательно итоговые значения альфы и цвета стремятся к Da и Dc. Т.е. пикселы там будут такие же, что и в черном фоне.

В итоге мы получим черный фон с прозрачным центром, а края останутся почти без изменений.

Реализуем это в коде. Перепишем метод init:

    private void init() {
      mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mShaderPaint.setShader(createShader());
      mShaderPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
      
      mBlackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mBlackPaint.setColor(Color.BLACK);
      mBlackPaint.setAlpha(100);
    }

Добавляем DST_OUT к кисти шейдера. И создаем кисть с полупрозрачным черным цветом.

 

и метод onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawRect(mRect, mBlackPaint);
      canvas.drawOval(mRectF, mShaderPaint);
    }

Выводим фон, а на него овал с шейдером.

 

Мы получили слой с рамкой прозрачности.

Центр этой рамки не белый, а прозрачный, через него просто просвечивает белый фон. 

 

Попробуем нарисовать все это на картинке сразу, без использования метода saveLayer.

Перепишем init:

    private void init() {
      mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mBitmap = BitmapFactory.decodeResource(getResources(),
          R.drawable.image);
      mBitmap = Bitmap.createScaledBitmap(mBitmap, mRect.width(),
          mRect.height(), true);

      mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mShaderPaint.setShader(createShader());
      mShaderPaint.setXfermode(new PorterDuffXfermode(
          PorterDuff.Mode.DST_OUT));

      mBlackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mBlackPaint.setColor(Color.BLACK);
      mBlackPaint.setAlpha(100);
    }

Добавляем создание картинки и обычной кисти для ее вывода.

 

и onDraw

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawBitmap(mBitmap, 0, 0, mPaint);
      canvas.drawRect(mRect, mBlackPaint);
      canvas.drawOval(mRectF, mShaderPaint);
    }

Рисуем сначала картинку, затем фон, затем овал.

 

Получилось не совсем то, что мы ожидали. Сама картинка в центре тоже стала прозрачной. Так произошло потому, что сначала на картинку мы нарисовали темный фон, получив просто затемненную картинку, а затем выполнили DST_OUT-наложение градиента. И это наложение повлияло на цвета и альфу самой картинки, сделав ее прозрачной в центре.

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

Перепишем onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawBitmap(mBitmap, 0, 0, mPaint);
      canvas.saveLayer(mRectF, mPaint, Canvas.ALL_SAVE_FLAG);
      canvas.drawRect(mRect, mBlackPaint);
      canvas.drawOval(mRectF, mShaderPaint);
      canvas.restore();
    }

Рисуем картинку. Затем переключаемся на отдельный слой методом saveLayer, рисуем на нем рамку (фон + овал с градиентом в режиме DST_OUT) и методом restore накладываем эту рамку на картинку. 

Результат:

Рамка легла сверху, обеспечив нужный уровень прозрачности и не затирая оригинал.

 

Если вы вместо метода saveLayer просто сами создадите Bitmap, нарисуете рамку на нем и потом просто наложите этот Bitmap поверх картинки, то вы получите тот же результат. В принципе, метод saveLayer именно это и делает, судя по его описанию в хелпе.


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal