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