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