1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.apppairs; 18 19 import android.graphics.Canvas; 20 import android.graphics.ColorFilter; 21 import android.graphics.Paint; 22 import android.graphics.PixelFormat; 23 import android.graphics.RectF; 24 import android.graphics.drawable.Drawable; 25 import android.os.Build; 26 27 import androidx.annotation.NonNull; 28 29 import com.android.launcher3.Utilities; 30 import com.android.launcher3.icons.FastBitmapDrawable; 31 32 /** 33 * A composed Drawable consisting of the two app pair icons and the background behind them (looks 34 * like two rectangles). 35 */ 36 public class AppPairIconDrawable extends Drawable { 37 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 38 private final AppPairIconDrawingParams mP; 39 private final FastBitmapDrawable mIcon1; 40 private final FastBitmapDrawable mIcon2; 41 42 /** 43 * Null values to use with 44 * {@link Canvas#drawDoubleRoundRect(RectF, float[], RectF, float[], Paint)}, since there 45 * doesn't seem to be any other API for drawing rectangles with 4 different corner radii. 46 */ 47 private static final RectF EMPTY_RECT = new RectF(); 48 private static final float[] ARRAY_OF_ZEROES = new float[8]; 49 AppPairIconDrawable( AppPairIconDrawingParams p, FastBitmapDrawable icon1, FastBitmapDrawable icon2)50 AppPairIconDrawable( 51 AppPairIconDrawingParams p, FastBitmapDrawable icon1, FastBitmapDrawable icon2) { 52 mP = p; 53 mBackgroundPaint.setStyle(Paint.Style.FILL); 54 mBackgroundPaint.setColor(p.getBgColor()); 55 mIcon1 = icon1; 56 mIcon2 = icon2; 57 } 58 59 @Override draw(@onNull Canvas canvas)60 public void draw(@NonNull Canvas canvas) { 61 if (mP.isLeftRightSplit()) { 62 drawLeftRightSplit(canvas); 63 } else { 64 drawTopBottomSplit(canvas); 65 } 66 67 canvas.translate( 68 mP.getStandardIconPadding() + mP.getOuterPadding(), 69 mP.getStandardIconPadding() + mP.getOuterPadding() 70 ); 71 72 // Draw first icon. 73 canvas.save(); 74 // The app icons are placed differently depending on device orientation. 75 if (mP.isLeftRightSplit()) { 76 canvas.translate( 77 mP.getInnerPadding(), 78 mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f 79 ); 80 } else { 81 canvas.translate( 82 mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f, 83 mP.getInnerPadding() 84 ); 85 } 86 87 mIcon1.draw(canvas); 88 canvas.restore(); 89 90 // Draw second icon. 91 canvas.save(); 92 // The app icons are placed differently depending on device orientation. 93 if (mP.isLeftRightSplit()) { 94 canvas.translate( 95 mP.getBackgroundSize() - (mP.getInnerPadding() + mP.getMemberIconSize()), 96 mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f 97 ); 98 } else { 99 canvas.translate( 100 mP.getBackgroundSize() / 2f - mP.getMemberIconSize() / 2f, 101 mP.getBackgroundSize() - (mP.getInnerPadding() + mP.getMemberIconSize()) 102 ); 103 } 104 105 mIcon2.draw(canvas); 106 canvas.restore(); 107 } 108 109 /** 110 * When device is in landscape, we draw the rectangles with a left-right split. 111 */ drawLeftRightSplit(Canvas canvas)112 private void drawLeftRightSplit(Canvas canvas) { 113 // Get the bounds where we will draw the background image 114 int width = mP.getIconSize(); 115 int height = mP.getIconSize(); 116 117 // The left half of the background image, excluding center channel 118 RectF leftSide = new RectF( 119 mP.getStandardIconPadding() + mP.getOuterPadding(), 120 mP.getStandardIconPadding() + mP.getOuterPadding(), 121 (width / 2f) - (mP.getCenterChannelSize() / 2f), 122 height - (mP.getStandardIconPadding() + mP.getOuterPadding()) 123 ); 124 // The right half of the background image, excluding center channel 125 RectF rightSide = new RectF( 126 (width / 2f) + (mP.getCenterChannelSize() / 2f), 127 (mP.getStandardIconPadding() + mP.getOuterPadding()), 128 width - (mP.getStandardIconPadding() + mP.getOuterPadding()), 129 height - (mP.getStandardIconPadding() + mP.getOuterPadding()) 130 ); 131 132 // Scale each background from its center edge closest to the center channel. 133 Utilities.scaleRectFAboutPivot( 134 leftSide, 135 leftSide.left + leftSide.width(), 136 leftSide.top + leftSide.centerY(), 137 mP.getHoverScale()); 138 Utilities.scaleRectFAboutPivot( 139 rightSide, 140 rightSide.left, 141 rightSide.top + rightSide.centerY(), 142 mP.getHoverScale()); 143 144 drawCustomRoundedRect(canvas, leftSide, new float[]{ 145 mP.getBigRadius(), mP.getBigRadius(), 146 mP.getSmallRadius(), mP.getSmallRadius(), 147 mP.getSmallRadius(), mP.getSmallRadius(), 148 mP.getBigRadius(), mP.getBigRadius()}); 149 drawCustomRoundedRect(canvas, rightSide, new float[]{ 150 mP.getSmallRadius(), mP.getSmallRadius(), 151 mP.getBigRadius(), mP.getBigRadius(), 152 mP.getBigRadius(), mP.getBigRadius(), 153 mP.getSmallRadius(), mP.getSmallRadius()}); 154 } 155 156 /** 157 * When device is in portrait, we draw the rectangles with a top-bottom split. 158 */ drawTopBottomSplit(Canvas canvas)159 private void drawTopBottomSplit(Canvas canvas) { 160 // Get the bounds where we will draw the background image 161 int width = mP.getIconSize(); 162 int height = mP.getIconSize(); 163 164 // The top half of the background image, excluding center channel 165 RectF topSide = new RectF( 166 (mP.getStandardIconPadding() + mP.getOuterPadding()), 167 (mP.getStandardIconPadding() + mP.getOuterPadding()), 168 width - (mP.getStandardIconPadding() + mP.getOuterPadding()), 169 (height / 2f) - (mP.getCenterChannelSize() / 2f) 170 ); 171 // The bottom half of the background image, excluding center channel 172 RectF bottomSide = new RectF( 173 (mP.getStandardIconPadding() + mP.getOuterPadding()), 174 (height / 2f) + (mP.getCenterChannelSize() / 2f), 175 width - (mP.getStandardIconPadding() + mP.getOuterPadding()), 176 height - (mP.getStandardIconPadding() + mP.getOuterPadding()) 177 ); 178 179 // Scale each background from its center edge closest to the center channel. 180 Utilities.scaleRectFAboutPivot( 181 topSide, 182 topSide.left + topSide.centerX(), 183 topSide.top + topSide.height(), 184 mP.getHoverScale()); 185 Utilities.scaleRectFAboutPivot( 186 bottomSide, 187 bottomSide.left + bottomSide.centerX(), 188 bottomSide.top, 189 mP.getHoverScale()); 190 191 drawCustomRoundedRect(canvas, topSide, new float[]{ 192 mP.getBigRadius(), mP.getBigRadius(), 193 mP.getBigRadius(), mP.getBigRadius(), 194 mP.getSmallRadius(), mP.getSmallRadius(), 195 mP.getSmallRadius(), mP.getSmallRadius()}); 196 drawCustomRoundedRect(canvas, bottomSide, new float[]{ 197 mP.getSmallRadius(), mP.getSmallRadius(), 198 mP.getSmallRadius(), mP.getSmallRadius(), 199 mP.getBigRadius(), mP.getBigRadius(), 200 mP.getBigRadius(), mP.getBigRadius()}); 201 } 202 203 /** 204 * Draws a rectangle with custom rounded corners. 205 * @param c The Canvas to draw on. 206 * @param rect The bounds of the rectangle. 207 * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top 208 * right y, bottom right x, and so on. 209 */ drawCustomRoundedRect(Canvas c, RectF rect, float[] radii)210 private void drawCustomRoundedRect(Canvas c, RectF rect, float[] radii) { 211 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 212 // Canvas.drawDoubleRoundRect is supported from Q onward 213 c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, mBackgroundPaint); 214 } else { 215 // Fallback rectangle with uniform rounded corners 216 c.drawRoundRect(rect, mP.getBigRadius(), mP.getBigRadius(), mBackgroundPaint); 217 } 218 } 219 220 @Override getOpacity()221 public int getOpacity() { 222 return PixelFormat.OPAQUE; 223 } 224 225 @Override setAlpha(int i)226 public void setAlpha(int i) { 227 mBackgroundPaint.setAlpha(i); 228 } 229 230 @Override setColorFilter(ColorFilter colorFilter)231 public void setColorFilter(ColorFilter colorFilter) { 232 mBackgroundPaint.setColorFilter(colorFilter); 233 } 234 235 @Override getIntrinsicWidth()236 public int getIntrinsicWidth() { 237 return mP.getIconSize(); 238 } 239 240 @Override getIntrinsicHeight()241 public int getIntrinsicHeight() { 242 return mP.getIconSize(); 243 } 244 } 245