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