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