1 /*
2  * Copyright (C) 2021 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 com.android.settingslib.widget;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.drawable.Animatable;
23 import android.graphics.drawable.Animatable2;
24 import android.graphics.drawable.AnimationDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewGroup.LayoutParams;
32 import android.widget.FrameLayout;
33 import android.widget.ImageView;
34 
35 import androidx.annotation.RawRes;
36 import androidx.preference.Preference;
37 import androidx.preference.PreferenceViewHolder;
38 import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
39 
40 import com.airbnb.lottie.LottieAnimationView;
41 import com.airbnb.lottie.LottieDrawable;
42 
43 import java.io.FileNotFoundException;
44 import java.io.InputStream;
45 
46 /**
47  * IllustrationPreference is a preference that can play lottie format animation
48  */
49 public class IllustrationPreference extends Preference {
50 
51     private static final String TAG = "IllustrationPreference";
52 
53     private static final boolean IS_ENABLED_LOTTIE_ADAPTIVE_COLOR = false;
54     private static final int SIZE_UNSPECIFIED = -1;
55 
56     private int mMaxHeight = SIZE_UNSPECIFIED;
57     private int mImageResId;
58     private boolean mCacheComposition = true;
59     private boolean mIsAutoScale;
60     private Uri mImageUri;
61     private Drawable mImageDrawable;
62     private View mMiddleGroundView;
63     private OnBindListener mOnBindListener;
64 
65     private boolean mLottieDynamicColor;
66 
67     /**
68      * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
69      */
70     public interface OnBindListener {
71         /**
72          * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
73          * @param animationView the animation view for this preference.
74          */
onBind(LottieAnimationView animationView)75         void onBind(LottieAnimationView animationView);
76     }
77 
78     private final Animatable2.AnimationCallback mAnimationCallback =
79             new Animatable2.AnimationCallback() {
80                 @Override
81                 public void onAnimationEnd(Drawable drawable) {
82                     ((Animatable) drawable).start();
83                 }
84             };
85 
86     private final Animatable2Compat.AnimationCallback mAnimationCallbackCompat =
87             new Animatable2Compat.AnimationCallback() {
88                 @Override
89                 public void onAnimationEnd(Drawable drawable) {
90                     ((Animatable) drawable).start();
91                 }
92             };
93 
IllustrationPreference(Context context)94     public IllustrationPreference(Context context) {
95         super(context);
96         init(context, /* attrs= */ null);
97     }
98 
IllustrationPreference(Context context, AttributeSet attrs)99     public IllustrationPreference(Context context, AttributeSet attrs) {
100         super(context, attrs);
101         init(context, attrs);
102     }
103 
IllustrationPreference(Context context, AttributeSet attrs, int defStyleAttr)104     public IllustrationPreference(Context context, AttributeSet attrs, int defStyleAttr) {
105         super(context, attrs, defStyleAttr);
106         init(context, attrs);
107     }
108 
IllustrationPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)109     public IllustrationPreference(Context context, AttributeSet attrs, int defStyleAttr,
110             int defStyleRes) {
111         super(context, attrs, defStyleAttr, defStyleRes);
112         init(context, attrs);
113     }
114 
115     @Override
onBindViewHolder(PreferenceViewHolder holder)116     public void onBindViewHolder(PreferenceViewHolder holder) {
117         super.onBindViewHolder(holder);
118 
119         final ImageView backgroundView =
120                 (ImageView) holder.findViewById(R.id.background_view);
121         final FrameLayout middleGroundLayout =
122                 (FrameLayout) holder.findViewById(R.id.middleground_layout);
123         final LottieAnimationView illustrationView =
124                 (LottieAnimationView) holder.findViewById(R.id.lottie_view);
125 
126         // To solve the problem of non-compliant illustrations, we set the frame height
127         // to 300dp and set the length of the short side of the screen to
128         // the width of the frame.
129         final int screenWidth = getContext().getResources().getDisplayMetrics().widthPixels;
130         final int screenHeight = getContext().getResources().getDisplayMetrics().heightPixels;
131         final FrameLayout illustrationFrame = (FrameLayout) holder.findViewById(
132                 R.id.illustration_frame);
133         final LayoutParams lp = (LayoutParams) illustrationFrame.getLayoutParams();
134         lp.width = screenWidth < screenHeight ? screenWidth : screenHeight;
135         illustrationFrame.setLayoutParams(lp);
136 
137         illustrationView.setCacheComposition(mCacheComposition);
138         handleImageWithAnimation(illustrationView);
139         handleImageFrameMaxHeight(backgroundView, illustrationView);
140 
141         if (mIsAutoScale) {
142             illustrationView.setScaleType(mIsAutoScale
143                             ? ImageView.ScaleType.CENTER_CROP
144                             : ImageView.ScaleType.CENTER_INSIDE);
145         }
146 
147         handleMiddleGroundView(middleGroundLayout);
148 
149         if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) {
150             ColorUtils.applyDynamicColors(getContext(), illustrationView);
151         }
152 
153         if (mLottieDynamicColor) {
154             LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
155         }
156 
157         if (mOnBindListener != null) {
158             mOnBindListener.onBind(illustrationView);
159         }
160     }
161 
162     /**
163      * Sets a listener to be notified when the views are binded.
164      */
165     public void setOnBindListener(OnBindListener listener) {
166         mOnBindListener = listener;
167     }
168 
169     /**
170      * Sets the middle ground view to preference. The user
171      * can overlay a view on top of the animation.
172      */
173     public void setMiddleGroundView(View view) {
174         if (view != mMiddleGroundView) {
175             mMiddleGroundView = view;
176             notifyChanged();
177         }
178     }
179 
180     /**
181      * Removes the middle ground view of preference.
182      */
183     public void removeMiddleGroundView() {
184         mMiddleGroundView = null;
185         notifyChanged();
186     }
187 
188     /**
189      * Enables the auto scale feature of animation view.
190      */
191     public void enableAnimationAutoScale(boolean enable) {
192         if (enable != mIsAutoScale) {
193             mIsAutoScale = enable;
194             notifyChanged();
195         }
196     }
197 
198     /**
199      * Sets the lottie illustration resource id.
200      */
201     public void setLottieAnimationResId(int resId) {
202         if (resId != mImageResId) {
203             resetImageResourceCache();
204             mImageResId = resId;
205             notifyChanged();
206         }
207     }
208 
209     /**
210      * Gets the lottie illustration resource id.
211      */
212     public int getLottieAnimationResId() {
213         return mImageResId;
214     }
215 
216     /**
217      * Sets the image drawable to display image in {@link LottieAnimationView}.
218      *
219      * @param imageDrawable the drawable of an image
220      */
221     public void setImageDrawable(Drawable imageDrawable) {
222         if (imageDrawable != mImageDrawable) {
223             resetImageResourceCache();
224             mImageDrawable = imageDrawable;
225             notifyChanged();
226         }
227     }
228 
229     /**
230      * Gets the image drawable from display image in {@link LottieAnimationView}.
231      *
232      * @return the drawable of an image
233      */
234     public Drawable getImageDrawable() {
235         return mImageDrawable;
236     }
237 
238     /**
239      * Sets the image uri to display image in {@link LottieAnimationView}.
240      *
241      * @param imageUri the Uri of an image
242      */
243     public void setImageUri(Uri imageUri) {
244         if (imageUri != mImageUri) {
245             resetImageResourceCache();
246             mImageUri = imageUri;
247             notifyChanged();
248         }
249     }
250 
251     /**
252      * Gets the image uri from display image in {@link LottieAnimationView}.
253      *
254      * @return the Uri of an image
255      */
256     public Uri getImageUri() {
257         return mImageUri;
258     }
259 
260     /**
261      * Sets the maximum height of the views, still use the specific one if the maximum height was
262      * larger than the specific height from XML.
263      *
264      * @param maxHeight the maximum height of the frame views in terms of pixels.
265      */
266     public void setMaxHeight(int maxHeight) {
267         if (maxHeight != mMaxHeight) {
268             mMaxHeight = maxHeight;
269             notifyChanged();
270         }
271     }
272 
273     /**
274      * Sets the lottie illustration apply dynamic color.
275      */
276     public void applyDynamicColor() {
277         mLottieDynamicColor = true;
278         notifyChanged();
279     }
280 
281     /**
282      * Return if the lottie illustration apply dynamic color or not.
283      */
284     public boolean isApplyDynamicColor() {
285         return mLottieDynamicColor;
286     }
287 
288     private void resetImageResourceCache() {
289         mImageDrawable = null;
290         mImageUri = null;
291         mImageResId = 0;
292     }
293 
294     private void handleMiddleGroundView(ViewGroup middleGroundLayout) {
295         middleGroundLayout.removeAllViews();
296 
297         if (mMiddleGroundView != null) {
298             middleGroundLayout.addView(mMiddleGroundView);
299             middleGroundLayout.setVisibility(View.VISIBLE);
300         } else {
301             middleGroundLayout.setVisibility(View.GONE);
302         }
303     }
304 
305     private void handleImageWithAnimation(LottieAnimationView illustrationView) {
306         if (mImageDrawable != null) {
307             resetAnimations(illustrationView);
308             illustrationView.setImageDrawable(mImageDrawable);
309             final Drawable drawable = illustrationView.getDrawable();
310             if (drawable != null) {
311                 startAnimation(drawable);
312             }
313         }
314 
315         if (mImageUri != null) {
316             resetAnimations(illustrationView);
317             illustrationView.setImageURI(mImageUri);
318             final Drawable drawable = illustrationView.getDrawable();
319             if (drawable != null) {
320                 startAnimation(drawable);
321             } else {
322                 // The lottie image from the raw folder also returns null because the ImageView
323                 // couldn't handle it now.
324                 startLottieAnimationWith(illustrationView, mImageUri);
325             }
326         }
327 
328         if (mImageResId > 0) {
329             resetAnimations(illustrationView);
330             illustrationView.setImageResource(mImageResId);
331             final Drawable drawable = illustrationView.getDrawable();
332             if (drawable != null) {
333                 startAnimation(drawable);
334             } else {
335                 // The lottie image from the raw folder also returns null because the ImageView
336                 // couldn't handle it now.
337                 startLottieAnimationWith(illustrationView, mImageResId);
338             }
339         }
340     }
341 
342     private void handleImageFrameMaxHeight(ImageView backgroundView, ImageView illustrationView) {
343         if (mMaxHeight == SIZE_UNSPECIFIED) {
344             return;
345         }
346 
347         final Resources res = backgroundView.getResources();
348         final int frameWidth = res.getDimensionPixelSize(R.dimen.settingslib_illustration_width);
349         final int frameHeight = res.getDimensionPixelSize(R.dimen.settingslib_illustration_height);
350         final int restrictedMaxHeight = Math.min(mMaxHeight, frameHeight);
351         backgroundView.setMaxHeight(restrictedMaxHeight);
352         illustrationView.setMaxHeight(restrictedMaxHeight);
353 
354         // Ensures the illustration view size is smaller than or equal to the background view size.
355         final float aspectRatio = (float) frameWidth / frameHeight;
356         illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio));
357     }
358 
359     private void startAnimation(Drawable drawable) {
360         if (!(drawable instanceof Animatable)) {
361             return;
362         }
363 
364         if (drawable instanceof Animatable2) {
365             ((Animatable2) drawable).registerAnimationCallback(mAnimationCallback);
366         } else if (drawable instanceof Animatable2Compat) {
367             ((Animatable2Compat) drawable).registerAnimationCallback(mAnimationCallbackCompat);
368         } else if (drawable instanceof AnimationDrawable) {
369             ((AnimationDrawable) drawable).setOneShot(false);
370         }
371 
372         ((Animatable) drawable).start();
373     }
374 
375     private static void startLottieAnimationWith(LottieAnimationView illustrationView,
376             Uri imageUri) {
377         final InputStream inputStream =
378                 getInputStreamFromUri(illustrationView.getContext(), imageUri);
379         illustrationView.setFailureListener(
380                 result -> Log.w(TAG, "Invalid illustration image uri: " + imageUri, result));
381         illustrationView.setAnimation(inputStream, /* cacheKey= */ null);
382         illustrationView.setRepeatCount(LottieDrawable.INFINITE);
383         illustrationView.playAnimation();
384     }
385 
386     private static void startLottieAnimationWith(LottieAnimationView illustrationView,
387             @RawRes int rawRes) {
388         illustrationView.setFailureListener(
389                 result -> Log.w(TAG, "Invalid illustration resource id: " + rawRes, result));
390         illustrationView.setAnimation(rawRes);
391         illustrationView.setRepeatCount(LottieDrawable.INFINITE);
392         illustrationView.playAnimation();
393     }
394 
395     private static void resetAnimations(LottieAnimationView illustrationView) {
396         resetAnimation(illustrationView.getDrawable());
397 
398         illustrationView.cancelAnimation();
399     }
400 
401     private static void resetAnimation(Drawable drawable) {
402         if (!(drawable instanceof Animatable)) {
403             return;
404         }
405 
406         if (drawable instanceof Animatable2) {
407             ((Animatable2) drawable).clearAnimationCallbacks();
408         } else if (drawable instanceof Animatable2Compat) {
409             ((Animatable2Compat) drawable).clearAnimationCallbacks();
410         }
411 
412         ((Animatable) drawable).stop();
413     }
414 
415     private static InputStream getInputStreamFromUri(Context context, Uri uri) {
416         try {
417             return context.getContentResolver().openInputStream(uri);
418         } catch (FileNotFoundException e) {
419             Log.w(TAG, "Cannot find content uri: " + uri, e);
420             return null;
421         }
422     }
423 
424     private void init(Context context, AttributeSet attrs) {
425         setLayoutResource(R.layout.illustration_preference);
426 
427         mIsAutoScale = false;
428         if (attrs != null) {
429             TypedArray a = context.obtainStyledAttributes(attrs,
430                     R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
431             mImageResId = a.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0);
432             mCacheComposition = a.getBoolean(
433                     R.styleable.LottieAnimationView_lottie_cacheComposition, true);
434 
435             a = context.obtainStyledAttributes(attrs,
436                     R.styleable.IllustrationPreference, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
437             mLottieDynamicColor = a.getBoolean(R.styleable.IllustrationPreference_dynamicColor,
438                     false);
439 
440             a.recycle();
441         }
442     }
443 }
444