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