1 /* 2 * Copyright (C) 2014 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 android.graphics.drawable.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.content.res.Resources.Theme; 25 import android.content.res.XmlResourceParser; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.PixelFormat; 31 import android.graphics.PorterDuff.Mode; 32 import android.graphics.PorterDuffColorFilter; 33 import android.graphics.cts.R; 34 import android.graphics.drawable.Drawable.ConstantState; 35 import android.graphics.drawable.VectorDrawable; 36 import androidx.annotation.Nullable; 37 import android.support.test.InstrumentationRegistry; 38 import android.support.test.filters.SmallTest; 39 import android.support.test.runner.AndroidJUnit4; 40 import android.util.AttributeSet; 41 import android.util.Log; 42 import android.util.Xml; 43 44 import org.junit.Before; 45 import org.junit.Test; 46 import org.junit.runner.RunWith; 47 import org.xmlpull.v1.XmlPullParser; 48 import org.xmlpull.v1.XmlPullParserException; 49 50 import java.io.File; 51 import java.io.FileOutputStream; 52 import java.io.IOException; 53 54 @SmallTest 55 @RunWith(AndroidJUnit4.class) 56 public class VectorDrawableTest { 57 private static final String LOGTAG = "VectorDrawableTest"; 58 59 // Separate the test assets into different groups such that we could isolate the issue faster. 60 // Some new APIs or bug fixes only exist in particular os version, such that we name the tests 61 // and associated assets with OS code name L, M, N etc... 62 private static final int[] BASIC_ICON_RES_IDS = new int[]{ 63 R.drawable.vector_icon_create, 64 R.drawable.vector_icon_delete, 65 R.drawable.vector_icon_heart, 66 R.drawable.vector_icon_schedule, 67 R.drawable.vector_icon_settings, 68 R.drawable.vector_icon_random_path_1, 69 R.drawable.vector_icon_random_path_2, 70 R.drawable.vector_icon_repeated_cq, 71 R.drawable.vector_icon_repeated_st, 72 R.drawable.vector_icon_repeated_a_1, 73 R.drawable.vector_icon_repeated_a_2, 74 R.drawable.vector_icon_clip_path_1, 75 }; 76 77 private static final int[] BASIC_GOLDEN_IMAGES = new int[] { 78 R.drawable.vector_icon_create_golden, 79 R.drawable.vector_icon_delete_golden, 80 R.drawable.vector_icon_heart_golden, 81 R.drawable.vector_icon_schedule_golden, 82 R.drawable.vector_icon_settings_golden, 83 R.drawable.vector_icon_random_path_1_golden, 84 R.drawable.vector_icon_random_path_2_golden, 85 R.drawable.vector_icon_repeated_cq_golden, 86 R.drawable.vector_icon_repeated_st_golden, 87 R.drawable.vector_icon_repeated_a_1_golden, 88 R.drawable.vector_icon_repeated_a_2_golden, 89 R.drawable.vector_icon_clip_path_1_golden, 90 }; 91 92 private static final int[] L_M_ICON_RES_IDS = new int[] { 93 R.drawable.vector_icon_transformation_1, 94 R.drawable.vector_icon_transformation_2, 95 R.drawable.vector_icon_transformation_3, 96 R.drawable.vector_icon_transformation_4, 97 R.drawable.vector_icon_transformation_5, 98 R.drawable.vector_icon_transformation_6, 99 R.drawable.vector_icon_render_order_1, 100 R.drawable.vector_icon_render_order_2, 101 R.drawable.vector_icon_stroke_1, 102 R.drawable.vector_icon_stroke_2, 103 R.drawable.vector_icon_stroke_3, 104 R.drawable.vector_icon_scale_1, 105 R.drawable.vector_icon_scale_2, 106 R.drawable.vector_icon_scale_3, 107 R.drawable.vector_icon_group_clip, 108 }; 109 110 private static final int[] L_M_GOLDEN_IMAGES = new int[] { 111 R.drawable.vector_icon_transformation_1_golden, 112 R.drawable.vector_icon_transformation_2_golden, 113 R.drawable.vector_icon_transformation_3_golden, 114 R.drawable.vector_icon_transformation_4_golden, 115 R.drawable.vector_icon_transformation_5_golden, 116 R.drawable.vector_icon_transformation_6_golden, 117 R.drawable.vector_icon_render_order_1_golden, 118 R.drawable.vector_icon_render_order_2_golden, 119 R.drawable.vector_icon_stroke_1_golden, 120 R.drawable.vector_icon_stroke_2_golden, 121 R.drawable.vector_icon_stroke_3_golden, 122 R.drawable.vector_icon_scale_1_golden, 123 R.drawable.vector_icon_scale_2_golden, 124 R.drawable.vector_icon_scale_3_golden, 125 R.drawable.vector_icon_group_clip_golden, 126 }; 127 128 private static final int[] N_ICON_RES_IDS = new int[] { 129 R.drawable.vector_icon_implicit_lineto, 130 R.drawable.vector_icon_arcto, 131 R.drawable.vector_icon_filltype_nonzero, 132 R.drawable.vector_icon_filltype_evenodd, 133 }; 134 135 private static final int[] N_GOLDEN_IMAGES = new int[] { 136 R.drawable.vector_icon_implicit_lineto_golden, 137 R.drawable.vector_icon_arcto_golden, 138 R.drawable.vector_icon_filltype_nonzero_golden, 139 R.drawable.vector_icon_filltype_evenodd_golden, 140 }; 141 142 private static final int[] GRADIENT_ICON_RES_IDS = new int[] { 143 R.drawable.vector_icon_gradient_1, 144 R.drawable.vector_icon_gradient_2, 145 R.drawable.vector_icon_gradient_3, 146 R.drawable.vector_icon_gradient_1_clamp, 147 R.drawable.vector_icon_gradient_2_repeat, 148 R.drawable.vector_icon_gradient_3_mirror, 149 }; 150 151 private static final int[] GRADIENT_GOLDEN_IMAGES = new int[] { 152 R.drawable.vector_icon_gradient_1_golden, 153 R.drawable.vector_icon_gradient_2_golden, 154 R.drawable.vector_icon_gradient_3_golden, 155 R.drawable.vector_icon_gradient_1_clamp_golden, 156 R.drawable.vector_icon_gradient_2_repeat_golden, 157 R.drawable.vector_icon_gradient_3_mirror_golden, 158 }; 159 160 private static final int[] STATEFUL_RES_IDS = new int[] { 161 // All these icons are using the same color state list, make sure it works for either 162 // the same drawable ID or different ID but same content. 163 R.drawable.vector_icon_state_list, 164 R.drawable.vector_icon_state_list, 165 R.drawable.vector_icon_state_list_2, 166 }; 167 168 private static final int[][] STATEFUL_GOLDEN_IMAGES = new int[][] { 169 { 170 R.drawable.vector_icon_state_list_golden, 171 R.drawable.vector_icon_state_list_golden, 172 R.drawable.vector_icon_state_list_2_golden 173 }, 174 { 175 R.drawable.vector_icon_state_list_pressed_golden, 176 R.drawable.vector_icon_state_list_pressed_golden, 177 R.drawable.vector_icon_state_list_2_pressed_golden 178 } 179 }; 180 181 private static final int[][] STATEFUL_STATE_SETS = new int[][] { 182 {}, 183 { android.R.attr.state_pressed } 184 }; 185 186 private static final int IMAGE_WIDTH = 64; 187 private static final int IMAGE_HEIGHT = 64; 188 189 private static final boolean DBG_DUMP_PNG = false; 190 191 private Resources mResources; 192 private Bitmap mBitmap; 193 private Canvas mCanvas; 194 private Context mContext; 195 196 @Before 197 public void setup() { 198 final int width = IMAGE_WIDTH; 199 final int height = IMAGE_HEIGHT; 200 201 mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 202 mCanvas = new Canvas(mBitmap); 203 mContext = InstrumentationRegistry.getTargetContext(); 204 mResources = mContext.getResources(); 205 } 206 207 @Test 208 public void testBasicVectorDrawables() throws XmlPullParserException, IOException { 209 verifyVectorDrawables(BASIC_ICON_RES_IDS, BASIC_GOLDEN_IMAGES, null); 210 } 211 212 @Test 213 public void testLMVectorDrawables() throws XmlPullParserException, IOException { 214 verifyVectorDrawables(L_M_ICON_RES_IDS, L_M_GOLDEN_IMAGES, null); 215 } 216 217 @Test 218 public void testNVectorDrawables() throws XmlPullParserException, IOException { 219 verifyVectorDrawables(N_ICON_RES_IDS, N_GOLDEN_IMAGES, null); 220 } 221 222 @Test 223 public void testVectorDrawableGradient() throws XmlPullParserException, IOException { 224 verifyVectorDrawables(GRADIENT_ICON_RES_IDS, GRADIENT_GOLDEN_IMAGES, null); 225 } 226 227 @Test 228 public void testColorStateList() throws XmlPullParserException, IOException { 229 for (int i = 0; i < STATEFUL_STATE_SETS.length; i++) { 230 verifyVectorDrawables( 231 STATEFUL_RES_IDS, STATEFUL_GOLDEN_IMAGES[i], STATEFUL_STATE_SETS[i]); 232 } 233 } 234 235 private void verifyVectorDrawables(int[] resIds, int[] goldenImages, int[] stateSet) 236 throws XmlPullParserException, IOException { 237 for (int i = 0; i < resIds.length; i++) { 238 VectorDrawable vectorDrawable = new VectorDrawable(); 239 vectorDrawable.setBounds(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT); 240 241 // Setup VectorDrawable from xml file and draw into the bitmap. 242 XmlPullParser parser = mResources.getXml(resIds[i]); 243 AttributeSet attrs = Xml.asAttributeSet(parser); 244 245 int type; 246 while ((type=parser.next()) != XmlPullParser.START_TAG 247 && type != XmlPullParser.END_DOCUMENT) { 248 // Empty loop 249 } 250 251 if (type != XmlPullParser.START_TAG) { 252 throw new XmlPullParserException("No start tag found"); 253 } 254 255 Theme theme = mResources.newTheme(); 256 theme.applyStyle(R.style.Theme_ThemedDrawableTest, true); 257 vectorDrawable.inflate(mResources, parser, attrs, theme); 258 259 if (stateSet != null) { 260 vectorDrawable.setState(stateSet); 261 } 262 263 mBitmap.eraseColor(0); 264 vectorDrawable.draw(mCanvas); 265 266 if (DBG_DUMP_PNG) { 267 String stateSetTitle = getTitleForStateSet(stateSet); 268 DrawableTestUtils.saveAutoNamedVectorDrawableIntoPNG(mContext, mBitmap, resIds[i], 269 stateSetTitle); 270 } else { 271 // Start to compare 272 Bitmap golden = BitmapFactory.decodeResource(mResources, goldenImages[i]); 273 DrawableTestUtils.compareImages(mResources.getString(resIds[i]), mBitmap, golden, 274 DrawableTestUtils.PIXEL_ERROR_THRESHOLD, 275 DrawableTestUtils.PIXEL_ERROR_COUNT_THRESHOLD, 276 DrawableTestUtils.PIXEL_ERROR_TOLERANCE); 277 278 } 279 } 280 } 281 282 /** 283 * Generates an underline-delimited list of states in a given state set. 284 * <p> 285 * For example, the array {@code {R.attr.state_pressed}} would return 286 * {@code "pressed"}. 287 * 288 * @param stateSet a state set 289 * @return a string representing the state set, or {@code null} if the state set is empty or 290 * {@code null} 291 */ 292 private @Nullable String getTitleForStateSet(int[] stateSet) { 293 if (stateSet == null || stateSet.length == 0) { 294 return null; 295 } 296 297 final StringBuilder builder = new StringBuilder(); 298 for (int i = 0; i < stateSet.length; i++) { 299 final String state = mResources.getResourceName(stateSet[i]); 300 final int stateIndex = state.indexOf("state_"); 301 if (stateIndex >= 0) { 302 builder.append(state.substring(stateIndex + 6)); 303 } else { 304 builder.append(stateSet[i]); 305 } 306 } 307 308 return builder.toString(); 309 } 310 311 @Test 312 public void testGetChangingConfigurations() { 313 VectorDrawable vectorDrawable = new VectorDrawable(); 314 ConstantState constantState = vectorDrawable.getConstantState(); 315 316 // default 317 assertEquals(0, constantState.getChangingConfigurations()); 318 assertEquals(0, vectorDrawable.getChangingConfigurations()); 319 320 // change the drawable's configuration does not affect the state's configuration 321 vectorDrawable.setChangingConfigurations(0xff); 322 assertEquals(0xff, vectorDrawable.getChangingConfigurations()); 323 assertEquals(0, constantState.getChangingConfigurations()); 324 325 // the state's configuration get refreshed 326 constantState = vectorDrawable.getConstantState(); 327 assertEquals(0xff, constantState.getChangingConfigurations()); 328 329 // set a new configuration to drawable 330 vectorDrawable.setChangingConfigurations(0xff00); 331 assertEquals(0xff, constantState.getChangingConfigurations()); 332 assertEquals(0xffff, vectorDrawable.getChangingConfigurations()); 333 } 334 335 @Test 336 public void testGetConstantState() { 337 VectorDrawable vectorDrawable = new VectorDrawable(); 338 ConstantState constantState = vectorDrawable.getConstantState(); 339 assertNotNull(constantState); 340 assertEquals(0, constantState.getChangingConfigurations()); 341 342 vectorDrawable.setChangingConfigurations(1); 343 constantState = vectorDrawable.getConstantState(); 344 assertNotNull(constantState); 345 assertEquals(1, constantState.getChangingConfigurations()); 346 } 347 348 @Test 349 public void testMutate() { 350 // d1 and d2 will be mutated, while d3 will not. 351 VectorDrawable d1 = (VectorDrawable) mResources.getDrawable(R.drawable.vector_icon_create); 352 VectorDrawable d2 = (VectorDrawable) mResources.getDrawable(R.drawable.vector_icon_create); 353 VectorDrawable d3 = (VectorDrawable) mResources.getDrawable(R.drawable.vector_icon_create); 354 int restoreAlpha = d1.getAlpha(); 355 356 try { 357 // verify bad behavior - modify before mutate pollutes other drawables 358 d1.setAlpha(0x80); 359 assertEquals(0x80, d1.getAlpha()); 360 assertEquals(0x80, d2.getAlpha()); 361 assertEquals(0x80, d3.getAlpha()); 362 363 d1.mutate(); 364 d1.setAlpha(0x40); 365 assertEquals(0x40, d1.getAlpha()); 366 assertEquals(0x80, d2.getAlpha()); 367 assertEquals(0x80, d3.getAlpha()); 368 369 d2.setAlpha(0x00); 370 d2.mutate(); 371 // Test that after mutating, the alpha value is copied over. 372 assertEquals(0x00, d2.getAlpha()); 373 374 d2.setAlpha(0x20); 375 assertEquals(0x40, d1.getAlpha()); 376 assertEquals(0x20, d2.getAlpha()); 377 assertEquals(0x00, d3.getAlpha()); 378 } finally { 379 mResources.getDrawable(R.drawable.vector_icon_create).setAlpha(restoreAlpha); 380 } 381 } 382 383 @Test 384 public void testColorFilter() { 385 PorterDuffColorFilter filter = new PorterDuffColorFilter(Color.RED, Mode.SRC_IN); 386 VectorDrawable vectorDrawable = new VectorDrawable(); 387 vectorDrawable.setColorFilter(filter); 388 389 assertEquals(filter, vectorDrawable.getColorFilter()); 390 } 391 392 @Test 393 public void testGetOpacity () throws XmlPullParserException, IOException { 394 VectorDrawable vectorDrawable = new VectorDrawable(); 395 396 assertEquals("Default alpha should be 255", 255, vectorDrawable.getAlpha()); 397 assertEquals("Default opacity should be TRANSLUCENT", PixelFormat.TRANSLUCENT, 398 vectorDrawable.getOpacity()); 399 400 vectorDrawable.setAlpha(0); 401 assertEquals("Alpha should be 0 now", 0, vectorDrawable.getAlpha()); 402 assertEquals("Opacity should be TRANSPARENT now", PixelFormat.TRANSPARENT, 403 vectorDrawable.getOpacity()); 404 } 405 406 @Test 407 public void testPreloadDensity() throws XmlPullParserException, IOException { 408 final int densityDpi = mResources.getConfiguration().densityDpi; 409 try { 410 DrawableTestUtils.setResourcesDensity(mResources, densityDpi); 411 verifyPreloadDensityInner(mResources, densityDpi); 412 } finally { 413 DrawableTestUtils.setResourcesDensity(mResources, densityDpi); 414 } 415 } 416 417 @Test 418 public void testPreloadDensity_tvdpi() throws XmlPullParserException, IOException { 419 final int densityDpi = mResources.getConfiguration().densityDpi; 420 try { 421 DrawableTestUtils.setResourcesDensity(mResources, 213); 422 verifyPreloadDensityInner(mResources, 213); 423 } finally { 424 DrawableTestUtils.setResourcesDensity(mResources, densityDpi); 425 } 426 } 427 428 private void verifyPreloadDensityInner(Resources res, int densityDpi) 429 throws XmlPullParserException, IOException { 430 // Capture initial state at default density. 431 final XmlResourceParser parser = DrawableTestUtils.getResourceParser( 432 res, R.drawable.vector_density); 433 final VectorDrawable preloadedDrawable = new VectorDrawable(); 434 preloadedDrawable.inflate(mResources, parser, Xml.asAttributeSet(parser)); 435 final ConstantState preloadedConstantState = preloadedDrawable.getConstantState(); 436 final int origWidth = preloadedDrawable.getIntrinsicWidth(); 437 438 // Set density to half of original. Unlike offsets, which are 439 // truncated, dimensions are rounded to the nearest pixel. 440 DrawableTestUtils.setResourcesDensity(res, densityDpi / 2); 441 final VectorDrawable halfDrawable = 442 (VectorDrawable) preloadedConstantState.newDrawable(res); 443 // NOTE: densityDpi may not be an even number, so account for *actual* scaling in asserts 444 final float approxHalf = (float)(densityDpi / 2) / densityDpi; 445 assertEquals(Math.round(origWidth * approxHalf), halfDrawable.getIntrinsicWidth()); 446 447 // Set density to double original. 448 DrawableTestUtils.setResourcesDensity(res, densityDpi * 2); 449 final VectorDrawable doubleDrawable = 450 (VectorDrawable) preloadedConstantState.newDrawable(res); 451 assertEquals(origWidth * 2, doubleDrawable.getIntrinsicWidth()); 452 453 // Restore original density. 454 DrawableTestUtils.setResourcesDensity(res, densityDpi); 455 final VectorDrawable origDrawable = 456 (VectorDrawable) preloadedConstantState.newDrawable(); 457 assertEquals(origWidth, origDrawable.getIntrinsicWidth()); 458 459 // Ensure theme density is applied correctly. 460 final Theme t = res.newTheme(); 461 halfDrawable.applyTheme(t); 462 assertEquals(origWidth, halfDrawable.getIntrinsicWidth()); 463 doubleDrawable.applyTheme(t); 464 assertEquals(origWidth, doubleDrawable.getIntrinsicWidth()); 465 } 466 } 467