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