В этом уроке:
- используем метод 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
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня