1/* 2 * Copyright (C) 2017 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 17package android.support.v4.graphics.drawable; 18 19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.content.Context; 22import android.content.Intent; 23import android.graphics.Bitmap; 24import android.graphics.BitmapShader; 25import android.graphics.Canvas; 26import android.graphics.Color; 27import android.graphics.Matrix; 28import android.graphics.Paint; 29import android.graphics.Shader; 30import android.graphics.drawable.Icon; 31import android.net.Uri; 32import android.os.Build; 33import android.support.annotation.DrawableRes; 34import android.support.annotation.RequiresApi; 35import android.support.annotation.RestrictTo; 36import android.support.annotation.VisibleForTesting; 37 38/** 39 * Helper for accessing features in {@link android.graphics.drawable.Icon}. 40 */ 41public class IconCompat { 42 43 // Ratio of expected size to actual icon size 44 private static final float ADAPTIVE_ICON_INSET_FACTOR = 1 / 4f; 45 private static final float DEFAULT_VIEW_PORT_SCALE = 1 / (1 + 2 * ADAPTIVE_ICON_INSET_FACTOR); 46 private static final float ICON_DIAMETER_FACTOR = 176f / 192; 47 private static final float BLUR_FACTOR = 0.5f / 48; 48 private static final float KEY_SHADOW_OFFSET_FACTOR = 1f / 48; 49 50 private static final int KEY_SHADOW_ALPHA = 61; 51 private static final int AMBIENT_SHADOW_ALPHA = 30; 52 53 private static final int TYPE_BITMAP = 1; 54 private static final int TYPE_RESOURCE = 2; 55 private static final int TYPE_DATA = 3; 56 private static final int TYPE_URI = 4; 57 private static final int TYPE_ADAPTIVE_BITMAP = 5; 58 59 private final int mType; 60 61 // To avoid adding unnecessary overhead, we have a few basic objects that get repurposed 62 // based on the value of mType. 63 64 // TYPE_BITMAP: Bitmap 65 // TYPE_ADAPTIVE_BITMAP: Bitmap 66 // TYPE_RESOURCE: Context 67 // TYPE_URI: String 68 // TYPE_DATA: DataBytes 69 private Object mObj1; 70 71 // TYPE_RESOURCE: resId 72 // TYPE_DATA: data offset 73 private int mInt1; 74 75 // TYPE_DATA: data length 76 private int mInt2; 77 78 /** 79 * Create an Icon pointing to a drawable resource. 80 * @param context The context for the application whose resources should be used to resolve the 81 * given resource ID. 82 * @param resId ID of the drawable resource 83 * @see android.graphics.drawable.Icon#createWithResource(Context, int) 84 */ 85 public static IconCompat createWithResource(Context context, @DrawableRes int resId) { 86 if (context == null) { 87 throw new IllegalArgumentException("Context must not be null."); 88 } 89 final IconCompat rep = new IconCompat(TYPE_RESOURCE); 90 rep.mInt1 = resId; 91 rep.mObj1 = context; 92 return rep; 93 } 94 95 /** 96 * Create an Icon pointing to a bitmap in memory. 97 * @param bits A valid {@link android.graphics.Bitmap} object 98 * @see android.graphics.drawable.Icon#createWithBitmap(Bitmap) 99 */ 100 public static IconCompat createWithBitmap(Bitmap bits) { 101 if (bits == null) { 102 throw new IllegalArgumentException("Bitmap must not be null."); 103 } 104 final IconCompat rep = new IconCompat(TYPE_BITMAP); 105 rep.mObj1 = bits; 106 return rep; 107 } 108 109 /** 110 * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined 111 * by {@link android.graphics.drawable.AdaptiveIconDrawable}. 112 * @param bits A valid {@link android.graphics.Bitmap} object 113 * @see android.graphics.drawable.Icon#createWithAdaptiveBitmap(Bitmap) 114 */ 115 public static IconCompat createWithAdaptiveBitmap(Bitmap bits) { 116 if (bits == null) { 117 throw new IllegalArgumentException("Bitmap must not be null."); 118 } 119 final IconCompat rep = new IconCompat(TYPE_ADAPTIVE_BITMAP); 120 rep.mObj1 = bits; 121 return rep; 122 } 123 124 /** 125 * Create an Icon pointing to a compressed bitmap stored in a byte array. 126 * @param data Byte array storing compressed bitmap data of a type that 127 * {@link android.graphics.BitmapFactory} 128 * can decode (see {@link android.graphics.Bitmap.CompressFormat}). 129 * @param offset Offset into <code>data</code> at which the bitmap data starts 130 * @param length Length of the bitmap data 131 * @see android.graphics.drawable.Icon#createWithData(byte[], int, int) 132 */ 133 public static IconCompat createWithData(byte[] data, int offset, int length) { 134 if (data == null) { 135 throw new IllegalArgumentException("Data must not be null."); 136 } 137 final IconCompat rep = new IconCompat(TYPE_DATA); 138 rep.mObj1 = data; 139 rep.mInt1 = offset; 140 rep.mInt2 = length; 141 return rep; 142 } 143 144 /** 145 * Create an Icon pointing to an image file specified by URI. 146 * 147 * @param uri A uri referring to local content:// or file:// image data. 148 * @see android.graphics.drawable.Icon#createWithContentUri(String) 149 */ 150 public static IconCompat createWithContentUri(String uri) { 151 if (uri == null) { 152 throw new IllegalArgumentException("Uri must not be null."); 153 } 154 final IconCompat rep = new IconCompat(TYPE_URI); 155 rep.mObj1 = uri; 156 return rep; 157 } 158 159 /** 160 * Create an Icon pointing to an image file specified by URI. 161 * 162 * @param uri A uri referring to local content:// or file:// image data. 163 * @see android.graphics.drawable.Icon#createWithContentUri(String) 164 */ 165 public static IconCompat createWithContentUri(Uri uri) { 166 if (uri == null) { 167 throw new IllegalArgumentException("Uri must not be null."); 168 } 169 return createWithContentUri(uri.toString()); 170 } 171 172 private IconCompat(int mType) { 173 this.mType = mType; 174 } 175 176 /** 177 * Convert this compat object to {@link Icon} object. 178 * 179 * @return {@link Icon} object 180 */ 181 @RequiresApi(23) 182 public Icon toIcon() { 183 switch (mType) { 184 case TYPE_BITMAP: 185 return Icon.createWithBitmap((Bitmap) mObj1); 186 case TYPE_ADAPTIVE_BITMAP: 187 if (Build.VERSION.SDK_INT >= 26) { 188 return Icon.createWithAdaptiveBitmap((Bitmap) mObj1); 189 } else { 190 return Icon.createWithBitmap(createLegacyIconFromAdaptiveIcon((Bitmap) mObj1)); 191 } 192 case TYPE_RESOURCE: 193 return Icon.createWithResource((Context) mObj1, mInt1); 194 case TYPE_DATA: 195 return Icon.createWithData((byte[]) mObj1, mInt1, mInt2); 196 case TYPE_URI: 197 return Icon.createWithContentUri((String) mObj1); 198 default: 199 throw new IllegalArgumentException("Unknown type"); 200 } 201 } 202 203 /** 204 * @hide 205 */ 206 @RestrictTo(LIBRARY_GROUP) 207 public void addToShortcutIntent(Intent outIntent) { 208 switch (mType) { 209 case TYPE_BITMAP: 210 outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, (Bitmap) mObj1); 211 break; 212 case TYPE_ADAPTIVE_BITMAP: 213 outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, 214 createLegacyIconFromAdaptiveIcon((Bitmap) mObj1)); 215 break; 216 case TYPE_RESOURCE: 217 outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 218 Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1)); 219 break; 220 default: 221 throw new IllegalArgumentException("Icon type not supported for intent shortcuts"); 222 } 223 } 224 225 /** 226 * Converts a bitmap following the adaptive icon guide lines, into a bitmap following the 227 * shortcut icon guide lines. 228 * The returned bitmap will always have same width and height and clipped to a circle. 229 */ 230 @VisibleForTesting 231 static Bitmap createLegacyIconFromAdaptiveIcon(Bitmap adaptiveIconBitmap) { 232 int size = (int) (DEFAULT_VIEW_PORT_SCALE * Math.min(adaptiveIconBitmap.getWidth(), 233 adaptiveIconBitmap.getHeight())); 234 235 Bitmap icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); 236 Canvas canvas = new Canvas(icon); 237 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 238 239 float center = size * 0.5f; 240 float radius = center * ICON_DIAMETER_FACTOR; 241 242 // Draw key shadow 243 float blur = BLUR_FACTOR * size; 244 paint.setColor(Color.TRANSPARENT); 245 paint.setShadowLayer(blur, 0, KEY_SHADOW_OFFSET_FACTOR * size, KEY_SHADOW_ALPHA << 24); 246 canvas.drawCircle(center, center, radius, paint); 247 248 // Draw ambient shadow 249 paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24); 250 canvas.drawCircle(center, center, radius, paint); 251 paint.clearShadowLayer(); 252 253 // Draw the clipped icon 254 paint.setColor(Color.BLACK); 255 BitmapShader shader = new BitmapShader(adaptiveIconBitmap, Shader.TileMode.CLAMP, 256 Shader.TileMode.CLAMP); 257 Matrix shift = new Matrix(); 258 shift.setTranslate(-(adaptiveIconBitmap.getWidth() - size) / 2, 259 -(adaptiveIconBitmap.getHeight() - size) / 2); 260 shader.setLocalMatrix(shift); 261 paint.setShader(shader); 262 canvas.drawCircle(center, center, radius, paint); 263 264 canvas.setBitmap(null); 265 return icon; 266 } 267} 268