1/* 2 * Copyright (C) 2015 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.graphics.drawable.tests; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.content.res.Resources.Theme; 22import android.graphics.Bitmap; 23import android.graphics.BitmapFactory; 24import android.graphics.Canvas; 25import android.graphics.Color; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.support.graphics.drawable.VectorDrawableCompat; 29import android.support.graphics.drawable.test.R; 30import android.support.test.InstrumentationRegistry; 31import android.support.test.runner.AndroidJUnit4; 32import android.test.suitebuilder.annotation.MediumTest; 33import android.util.Log; 34import org.junit.Before; 35import org.junit.Test; 36import org.junit.runner.RunWith; 37import org.xmlpull.v1.XmlPullParserException; 38 39import java.io.File; 40import java.io.FileOutputStream; 41import java.io.IOException; 42 43import static org.junit.Assert.*; 44 45@RunWith(AndroidJUnit4.class) 46@MediumTest 47public class VectorDrawableTest { 48 private static final String LOGTAG = "VectorDrawableTest"; 49 50 private static final int[] ICON_RES_IDS = new int[]{ 51 R.drawable.vector_icon_create, 52 R.drawable.vector_icon_delete, 53 R.drawable.vector_icon_heart, 54 R.drawable.vector_icon_schedule, 55 R.drawable.vector_icon_settings, 56 R.drawable.vector_icon_random_path_1, 57 R.drawable.vector_icon_random_path_2, 58 R.drawable.vector_icon_repeated_cq, 59 R.drawable.vector_icon_repeated_st, 60 R.drawable.vector_icon_repeated_a_1, 61 R.drawable.vector_icon_repeated_a_2, 62 R.drawable.vector_icon_clip_path_1, 63 R.drawable.vector_icon_transformation_1, 64 R.drawable.vector_icon_transformation_4, 65 R.drawable.vector_icon_transformation_5, 66 R.drawable.vector_icon_transformation_6, 67 R.drawable.vector_icon_render_order_1, 68 R.drawable.vector_icon_render_order_2, 69 R.drawable.vector_icon_stroke_1, 70 R.drawable.vector_icon_stroke_2, 71 R.drawable.vector_icon_stroke_3, 72 R.drawable.vector_icon_scale_1, 73 R.drawable.vector_icon_group_clip, 74 R.drawable.vector_icon_share, 75 R.drawable.vector_icon_wishlist, 76 R.drawable.vector_icon_five_bars, 77 }; 78 79 private static final int[] GOLDEN_IMAGES = new int[]{ 80 R.drawable.vector_icon_create_golden, 81 R.drawable.vector_icon_delete_golden, 82 R.drawable.vector_icon_heart_golden, 83 R.drawable.vector_icon_schedule_golden, 84 R.drawable.vector_icon_settings_golden, 85 R.drawable.vector_icon_random_path_1_golden, 86 R.drawable.vector_icon_random_path_2_golden, 87 R.drawable.vector_icon_repeated_cq_golden, 88 R.drawable.vector_icon_repeated_st_golden, 89 R.drawable.vector_icon_repeated_a_1_golden, 90 R.drawable.vector_icon_repeated_a_2_golden, 91 R.drawable.vector_icon_clip_path_1_golden, 92 R.drawable.vector_icon_transformation_1_golden, 93 R.drawable.vector_icon_transformation_4_golden, 94 R.drawable.vector_icon_transformation_5_golden, 95 R.drawable.vector_icon_transformation_6_golden, 96 R.drawable.vector_icon_render_order_1_golden, 97 R.drawable.vector_icon_render_order_2_golden, 98 R.drawable.vector_icon_stroke_1_golden, 99 R.drawable.vector_icon_stroke_2_golden, 100 R.drawable.vector_icon_stroke_3_golden, 101 R.drawable.vector_icon_scale_1_golden, 102 R.drawable.vector_icon_group_clip_golden, 103 R.drawable.vector_icon_share_golden, 104 R.drawable.vector_icon_wishlist_golden, 105 R.drawable.vector_icon_five_bars_golden, 106 }; 107 108 private static final int TEST_ICON = R.drawable.vector_icon_create; 109 110 private static final int IMAGE_WIDTH = 64; 111 private static final int IMAGE_HEIGHT = 64; 112 // A small value is actually making sure that the values are matching 113 // exactly with the golden image. 114 // We can increase the threshold if the Skia is drawing with some variance 115 // on different devices. So far, the tests show they are matching correctly. 116 private static final float PIXEL_ERROR_THRESHOLD = 0.3f; 117 private static final float PIXEL_DIFF_COUNT_THRESHOLD = 0.1f; 118 private static final float PIXEL_DIFF_THRESHOLD = 0.025f; 119 120 private static final boolean DBG_DUMP_PNG = false; 121 122 private Context mContext; 123 private Resources mResources; 124 private VectorDrawableCompat mVectorDrawable; 125 private Bitmap mBitmap; 126 private Canvas mCanvas; 127 private Theme mTheme; 128 129 @Before 130 public void setup() { 131 final int width = IMAGE_WIDTH; 132 final int height = IMAGE_HEIGHT; 133 134 mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 135 mCanvas = new Canvas(mBitmap); 136 137 mContext = InstrumentationRegistry.getContext(); 138 mResources = mContext.getResources(); 139 mTheme = mContext.getTheme(); 140 } 141 142 @Test 143 public void testSimpleVectorDrawables() throws Exception { 144 verifyVectorDrawables(ICON_RES_IDS, GOLDEN_IMAGES, null); 145 } 146 147 private void verifyVectorDrawables(int[] resIds, int[] goldenImages, int[] stateSet) 148 throws XmlPullParserException, IOException { 149 for (int i = 0; i < resIds.length; i++) { 150 // Setup VectorDrawable from xml file and draw into the bitmap. 151 mVectorDrawable = VectorDrawableCompat.create(mResources, resIds[i], mTheme); 152 mVectorDrawable.setBounds(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT); 153 if (stateSet != null) { 154 mVectorDrawable.setState(stateSet); 155 } 156 157 mBitmap.eraseColor(0); 158 mVectorDrawable.draw(mCanvas); 159 160 if (DBG_DUMP_PNG) { 161 saveVectorDrawableIntoPNG(mBitmap, resIds, i, stateSet); 162 } else { 163 // Start to compare 164 Bitmap golden = BitmapFactory.decodeResource(mResources, goldenImages[i]); 165 compareImages(mBitmap, golden, mResources.getString(resIds[i])); 166 } 167 } 168 } 169 170 // This is only for debugging or golden image (re)generation purpose. 171 private void saveVectorDrawableIntoPNG(Bitmap bitmap, int[] resIds, int index, int[] stateSet) 172 throws IOException { 173 // Save the image to the disk. 174 FileOutputStream out = null; 175 try { 176 String outputFolder = "/sdcard/temp/"; 177 File folder = new File(outputFolder); 178 if (!folder.exists()) { 179 folder.mkdir(); 180 } 181 String originalFilePath = mResources.getString(resIds[index]); 182 File originalFile = new File(originalFilePath); 183 String fileFullName = originalFile.getName(); 184 String fileTitle = fileFullName.substring(0, fileFullName.lastIndexOf(".")); 185 String stateSetTitle = getTitleForStateSet(stateSet); 186 String outputFilename = outputFolder + fileTitle + "_golden" + stateSetTitle + ".png"; 187 File outputFile = new File(outputFilename); 188 if (!outputFile.exists()) { 189 outputFile.createNewFile(); 190 } 191 192 out = new FileOutputStream(outputFile, false); 193 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 194 Log.v(LOGTAG, "Write test No." + index + " to file successfully."); 195 } catch (Exception e) { 196 e.printStackTrace(); 197 } finally { 198 if (out != null) { 199 out.close(); 200 } 201 } 202 } 203 204 /** 205 * Generates an underline-delimited list of states in a given state set. 206 * <p/> 207 * For example, the array {@code {R.attr.state_pressed}} would return 208 * {@code "_pressed"}. 209 * 210 * @param stateSet a state set 211 * @return a string representing the state set, or the empty string if the 212 * state set is empty or {@code null} 213 */ 214 private String getTitleForStateSet(int[] stateSet) { 215 if (stateSet == null || stateSet.length == 0) { 216 return ""; 217 } 218 219 final StringBuilder builder = new StringBuilder(); 220 for (int i = 0; i < stateSet.length; i++) { 221 builder.append('_'); 222 223 final String state = mResources.getResourceName(stateSet[i]); 224 final int stateIndex = state.indexOf("state_"); 225 if (stateIndex >= 0) { 226 builder.append(state.substring(stateIndex + 6)); 227 } else { 228 builder.append(stateSet[i]); 229 } 230 } 231 232 return builder.toString(); 233 } 234 235 private void compareImages(Bitmap ideal, Bitmap given, String filename) { 236 int idealWidth = ideal.getWidth(); 237 int idealHeight = ideal.getHeight(); 238 239 assertTrue(idealWidth == given.getWidth()); 240 assertTrue(idealHeight == given.getHeight()); 241 242 int totalDiffPixelCount = 0; 243 float totalPixelCount = idealWidth * idealHeight; 244 for (int x = 0; x < idealWidth; x++) { 245 for (int y = 0; y < idealHeight; y++) { 246 int idealColor = ideal.getPixel(x, y); 247 int givenColor = given.getPixel(x, y); 248 if (idealColor == givenColor) 249 continue; 250 251 float totalError = 0; 252 totalError += Math.abs(Color.red(idealColor) - Color.red(givenColor)); 253 totalError += Math.abs(Color.green(idealColor) - Color.green(givenColor)); 254 totalError += Math.abs(Color.blue(idealColor) - Color.blue(givenColor)); 255 totalError += Math.abs(Color.alpha(idealColor) - Color.alpha(givenColor)); 256 257 if ((totalError / 1024.0f) >= PIXEL_ERROR_THRESHOLD) { 258 fail((filename + ": totalError is " + totalError)); 259 } 260 261 if ((totalError / 1024.0f) >= PIXEL_DIFF_THRESHOLD) { 262 totalDiffPixelCount++; 263 } 264 } 265 } 266 if ((totalDiffPixelCount / totalPixelCount) >= PIXEL_DIFF_COUNT_THRESHOLD) { 267 fail((filename + ": totalDiffPixelCount is " + totalDiffPixelCount)); 268 } 269 270 } 271 272 @Test 273 public void testGetChangingConfigurations() { 274 VectorDrawableCompat vectorDrawable = 275 VectorDrawableCompat.create(mResources, TEST_ICON, mTheme); 276 Drawable.ConstantState constantState = vectorDrawable.getConstantState(); 277 278 // default 279 assertEquals(0, constantState.getChangingConfigurations()); 280 assertEquals(0, vectorDrawable.getChangingConfigurations()); 281 282 // change the drawable's configuration does not affect the state's configuration 283 vectorDrawable.setChangingConfigurations(0xff); 284 assertEquals(0xff, vectorDrawable.getChangingConfigurations()); 285 assertEquals(0, constantState.getChangingConfigurations()); 286 287 // the state's configuration get refreshed 288 constantState = vectorDrawable.getConstantState(); 289 assertEquals(0xff, constantState.getChangingConfigurations()); 290 291 // set a new configuration to drawable 292 vectorDrawable.setChangingConfigurations(0xff00); 293 assertEquals(0xff, constantState.getChangingConfigurations()); 294 assertEquals(0xffff, vectorDrawable.getChangingConfigurations()); 295 } 296 297 @Test 298 public void testGetConstantState() { 299 VectorDrawableCompat vectorDrawable = 300 VectorDrawableCompat.create(mResources, R.drawable.vector_icon_delete, mTheme); 301 Drawable.ConstantState constantState = vectorDrawable.getConstantState(); 302 assertNotNull(constantState); 303 assertEquals(0, constantState.getChangingConfigurations()); 304 305 vectorDrawable.setChangingConfigurations(1); 306 constantState = vectorDrawable.getConstantState(); 307 assertNotNull(constantState); 308 assertEquals(1, constantState.getChangingConfigurations()); 309 } 310 311 @Test 312 public void testMutate() { 313 VectorDrawableCompat d1 = 314 VectorDrawableCompat.create(mResources, TEST_ICON, mTheme); 315 VectorDrawableCompat d2 = 316 (VectorDrawableCompat) d1.getConstantState().newDrawable(mResources); 317 VectorDrawableCompat d3 = 318 (VectorDrawableCompat) d1.getConstantState().newDrawable(mResources); 319 320 // d1 will be mutated, while d2 / d3 will not. 321 int originalAlpha = d2.getAlpha(); 322 323 d1.setAlpha(0x80); 324 assertEquals(0x80, d1.getAlpha()); 325 assertEquals(0x80, d2.getAlpha()); 326 assertEquals(0x80, d3.getAlpha()); 327 328 d1.mutate(); 329 d1.setAlpha(0x40); 330 assertEquals(0x40, d1.getAlpha()); 331 assertEquals(0x80, d2.getAlpha()); 332 assertEquals(0x80, d3.getAlpha()); 333 334 d2.setAlpha(0x20); 335 assertEquals(0x40, d1.getAlpha()); 336 assertEquals(0x20, d2.getAlpha()); 337 assertEquals(0x20, d3.getAlpha()); 338 339 d2.setAlpha(originalAlpha); 340 } 341 342 public void testBounds() { 343 VectorDrawableCompat vectorDrawable = 344 VectorDrawableCompat.create(mResources, R.drawable.vector_icon_delete, mTheme); 345 Rect expectedRect = new Rect(0, 0, 100, 100); 346 vectorDrawable.setBounds(0, 0, 100, 100); 347 Rect rect = vectorDrawable.getBounds(); 348 assertEquals("Bounds should be same value for setBound(int ...)", rect, expectedRect); 349 350 vectorDrawable.setBounds(expectedRect); 351 rect = vectorDrawable.getBounds(); 352 assertEquals("Bounds should be same value for setBound(Rect)", rect, expectedRect); 353 354 vectorDrawable.copyBounds(rect); 355 assertEquals("Bounds should be same value for copyBounds", rect, expectedRect); 356 } 357} 358