blob: d239e3da89188a7bb7e486e93a4185a0f15e46cd [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupdesign;
import static com.google.android.setupcompat.partnerconfig.Util.isNightMode;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings.Global;
import android.provider.Settings.SettingNotFoundException;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RawRes;
import androidx.annotation.StringDef;
import androidx.annotation.VisibleForTesting;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.SimpleColorFilter;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.value.LottieValueCallback;
import com.airbnb.lottie.value.SimpleLottieValueCallback;
import com.google.android.setupcompat.partnerconfig.PartnerConfig;
import com.google.android.setupcompat.partnerconfig.PartnerConfig.ResourceType;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
import com.google.android.setupcompat.partnerconfig.ResourceEntry;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.util.BuildCompatUtils;
import com.google.android.setupdesign.lottieloadinglayout.R;
import com.google.android.setupdesign.view.IllustrationVideoView;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A GLIF themed layout with a {@link com.airbnb.lottie.LottieAnimationView} to showing lottie
* illustration and a substitute {@link com.google.android.setupdesign.view.IllustrationVideoView}
* to showing mp4 illustration. {@code app:sudIllustrationType} can also be used to specify one of
* the set including "default", "account", "connection", "update", and "final_hold". {@code
* app:sudLottieRes} can assign the json file of Lottie resource.
*/
public class GlifLoadingLayout extends GlifLayout {
private static final String TAG = "GlifLoadingLayout";
View inflatedView;
@VisibleForTesting @IllustrationType String illustrationType = IllustrationType.DEFAULT;
@VisibleForTesting LottieAnimationConfig animationConfig = LottieAnimationConfig.CONFIG_DEFAULT;
@VisibleForTesting @RawRes int customLottieResource = 0;
@VisibleForTesting Map<KeyPath, SimpleColorFilter> customizationMap = new HashMap<>();
@VisibleForTesting
public List<LottieAnimationFinishListener> animationFinishListeners = new ArrayList<>();
public GlifLoadingLayout(Context context) {
this(context, 0, 0);
}
public GlifLoadingLayout(Context context, int template) {
this(context, template, 0);
}
public GlifLoadingLayout(Context context, int template, int containerId) {
super(context, template, containerId);
init(null, R.attr.sudLayoutTheme);
}
public GlifLoadingLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, R.attr.sudLayoutTheme);
}
@TargetApi(VERSION_CODES.HONEYCOMB)
public GlifLoadingLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr);
}
private void init(AttributeSet attrs, int defStyleAttr) {
registerMixin(FooterBarMixin.class, new LoadingFooterBarMixin(this, attrs, defStyleAttr));
TypedArray a =
getContext()
.obtainStyledAttributes(attrs, R.styleable.SudGlifLoadingLayout, defStyleAttr, 0);
customLottieResource = a.getResourceId(R.styleable.SudGlifLoadingLayout_sudLottieRes, 0);
String illustrationType = a.getString(R.styleable.SudGlifLoadingLayout_sudIllustrationType);
boolean usePartnerHeavyTheme =
a.getBoolean(R.styleable.SudGlifLoadingLayout_sudUsePartnerHeavyTheme, false);
a.recycle();
if (customLottieResource != 0) {
inflateLottieView();
ViewGroup container = findContainer(0);
container.setVisibility(View.VISIBLE);
} else {
if (illustrationType != null) {
setIllustrationType(illustrationType);
}
if (BuildCompatUtils.isAtLeastS()) {
inflateLottieView();
} else {
inflateIllustrationStub();
}
}
boolean applyPartnerHeavyThemeResource = shouldApplyPartnerResource() && usePartnerHeavyTheme;
if (applyPartnerHeavyThemeResource) {
View view = findManagedViewById(R.id.sud_layout_loading_content);
if (view != null) {
applyPartnerCustomizationContentPaddingTopStyle(view);
}
}
updateHeaderHeight();
updateLandscapeMiddleHorizontalSpacing();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (inflatedView instanceof LinearLayout) {
updateContentPadding((LinearLayout) inflatedView);
}
}
public void setIllustrationType(@IllustrationType String type) {
if (customLottieResource != 0) {
throw new IllegalStateException(
"custom illustration already applied, should not set illustration.");
}
if (!illustrationType.equals(type)) {
illustrationType = type;
customizationMap.clear();
}
switch (type) {
case IllustrationType.ACCOUNT:
animationConfig = LottieAnimationConfig.CONFIG_ACCOUNT;
break;
case IllustrationType.CONNECTION:
animationConfig = LottieAnimationConfig.CONFIG_CONNECTION;
break;
case IllustrationType.UPDATE:
animationConfig = LottieAnimationConfig.CONFIG_UPDATE;
break;
case IllustrationType.FINAL_HOLD:
animationConfig = LottieAnimationConfig.CONFIG_FINAL_HOLD;
break;
default:
animationConfig = LottieAnimationConfig.CONFIG_DEFAULT;
break;
}
updateAnimationView();
}
// TODO: [GlifLoadingLayout] Should add testcase. LottieAnimationView was auto
// generated not able to mock. So we have no idea how to detected is the api pass to
// LottiAnimationView correctly.
public boolean setAnimation(InputStream inputStream, String keyCache) {
LottieAnimationView lottieAnimationView = findLottieAnimationView();
if (lottieAnimationView != null) {
lottieAnimationView.setAnimation(inputStream, keyCache);
return true;
} else {
return false;
}
}
public boolean setAnimation(String assetName) {
LottieAnimationView lottieAnimationView = findLottieAnimationView();
if (lottieAnimationView != null) {
lottieAnimationView.setAnimation(assetName);
return true;
} else {
return false;
}
}
public boolean setAnimation(@RawRes int rawRes) {
LottieAnimationView lottieAnimationView = findLottieAnimationView();
if (lottieAnimationView != null) {
lottieAnimationView.setAnimation(rawRes);
return true;
} else {
return false;
}
}
private void updateAnimationView() {
if (BuildCompatUtils.isAtLeastS()) {
setLottieResource();
} else {
setIllustrationResource();
}
}
/**
* Call this when your activity is done and should be closed. The activity will be finished while
* animation finished.
*/
public void finish(@NonNull Activity activity) {
if (activity == null) {
throw new NullPointerException("activity should not be null");
}
registerAnimationFinishRunnable(activity::finish, /* allowFinishWithMaximumDuration= */ true);
}
/**
* Launch a new activity after the animation finished.
*
* @param activity The activity which is GlifLoadingLayout attached to.
* @param intent The intent to start.
* @param options Additional options for how the Activity should be started. See {@link
* android.content.Context#startActivity(Intent, Bundle)} for more details.
* @param finish Finish the activity after startActivity
* @see Activity#startActivity(Intent)
* @see Activity#startActivityForResult
*/
public void startActivity(
@NonNull Activity activity,
@NonNull Intent intent,
@Nullable Bundle options,
boolean finish) {
if (activity == null) {
throw new NullPointerException("activity should not be null");
}
if (intent == null) {
throw new NullPointerException("intent should not be null");
}
registerAnimationFinishRunnable(
() -> {
if (options == null || Build.VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN) {
activity.startActivity(intent);
} else {
activity.startActivity(intent, options);
}
if (finish) {
activity.finish();
}
},
/* allowFinishWithMaximumDuration= */ true);
}
/**
* Waiting for the animation finished and launch an activity for which you would like a result
* when it finished.
*
* @param activity The activity which the GlifLoadingLayout attached to.
* @param intent The intent to start.
* @param requestCode If >= 0, this code will be returned in onActivityResult() when the activity
* exits.
* @param options Additional options for how the Activity should be started.
* @param finish Finish the activity after startActivityForResult. The onActivityResult might not
* be called because the activity already finished.
* <p>See {@link android.content.Context#startActivity(Intent, Bundle)}
* Context.startActivity(Intent, Bundle)} for more details.
*/
public void startActivityForResult(
@NonNull Activity activity,
@NonNull Intent intent,
int requestCode,
@Nullable Bundle options,
boolean finish) {
if (activity == null) {
throw new NullPointerException("activity should not be null");
}
if (intent == null) {
throw new NullPointerException("intent should not be null");
}
registerAnimationFinishRunnable(
() -> {
if (options == null || Build.VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN) {
activity.startActivityForResult(intent, requestCode);
} else {
activity.startActivityForResult(intent, requestCode, options);
}
if (finish) {
activity.finish();
}
},
/* allowFinishWithMaximumDuration= */ true);
}
private void updateHeaderHeight() {
View headerView = findManagedViewById(R.id.sud_header_scroll_view);
if (headerView != null
&& PartnerConfigHelper.get(getContext())
.isPartnerConfigAvailable(PartnerConfig.CONFIG_LOADING_LAYOUT_HEADER_HEIGHT)) {
float configHeaderHeight =
PartnerConfigHelper.get(getContext())
.getDimension(getContext(), PartnerConfig.CONFIG_LOADING_LAYOUT_HEADER_HEIGHT);
headerView.getLayoutParams().height = (int) configHeaderHeight;
}
}
private void updateContentPadding(LinearLayout linearLayout) {
int paddingTop = linearLayout.getPaddingTop();
int paddingLeft = linearLayout.getPaddingLeft();
int paddingRight = linearLayout.getPaddingRight();
int paddingBottom = linearLayout.getPaddingBottom();
if (PartnerConfigHelper.get(getContext())
.isPartnerConfigAvailable(PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_TOP)) {
float configPaddingTop =
PartnerConfigHelper.get(getContext())
.getDimension(getContext(), PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_TOP);
if (configPaddingTop >= 0) {
paddingTop = (int) configPaddingTop;
}
}
if (PartnerConfigHelper.get(getContext())
.isPartnerConfigAvailable(PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_START)) {
float configPaddingLeft =
PartnerConfigHelper.get(getContext())
.getDimension(getContext(), PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_START);
if (configPaddingLeft >= 0) {
paddingLeft = (int) configPaddingLeft;
}
}
if (PartnerConfigHelper.get(getContext())
.isPartnerConfigAvailable(PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_END)) {
float configPaddingRight =
PartnerConfigHelper.get(getContext())
.getDimension(getContext(), PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_END);
if (configPaddingRight >= 0) {
paddingRight = (int) configPaddingRight;
}
}
if (PartnerConfigHelper.get(getContext())
.isPartnerConfigAvailable(PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_BOTTOM)) {
float configPaddingBottom =
PartnerConfigHelper.get(getContext())
.getDimension(getContext(), PartnerConfig.CONFIG_LOADING_LAYOUT_PADDING_BOTTOM);
if (configPaddingBottom >= 0) {
FooterBarMixin footerBarMixin = getMixin(FooterBarMixin.class);
if (footerBarMixin == null || footerBarMixin.getButtonContainer() == null) {
paddingBottom = (int) configPaddingBottom;
} else {
paddingBottom =
(int) configPaddingBottom
- (int)
Math.min(
configPaddingBottom,
getButtonContainerHeight(footerBarMixin.getButtonContainer()));
}
}
}
linearLayout.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
private static final int getButtonContainerHeight(View view) {
view.measure(
MeasureSpec.makeMeasureSpec(view.getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(view.getMeasuredHeight(), MeasureSpec.EXACTLY));
return view.getMeasuredHeight();
}
private void inflateLottieView() {
final View lottieLayout = peekLottieLayout();
if (lottieLayout == null) {
ViewStub viewStub = findManagedViewById(R.id.sud_loading_layout_lottie_stub);
if (viewStub != null) {
inflatedView = viewStub.inflate();
if (inflatedView instanceof LinearLayout) {
updateContentPadding((LinearLayout) inflatedView);
}
setLottieResource();
}
}
}
private void inflateIllustrationStub() {
final View progressLayout = peekProgressIllustrationLayout();
if (progressLayout == null) {
ViewStub viewStub = findManagedViewById(R.id.sud_loading_layout_illustration_stub);
if (viewStub != null) {
inflatedView = viewStub.inflate();
if (inflatedView instanceof LinearLayout) {
updateContentPadding((LinearLayout) inflatedView);
}
setIllustrationResource();
}
}
}
private void setLottieResource() {
LottieAnimationView lottieView = findViewById(R.id.sud_lottie_view);
if (lottieView == null) {
Log.w(TAG, "Lottie view not found, skip set resource. Wait for layout inflated.");
return;
}
if (customLottieResource != 0) {
InputStream inputRaw = getResources().openRawResource(customLottieResource);
lottieView.setAnimation(inputRaw, null);
lottieView.playAnimation();
} else {
PartnerConfigHelper partnerConfigHelper = PartnerConfigHelper.get(getContext());
ResourceEntry resourceEntry =
partnerConfigHelper.getIllustrationResourceEntry(
getContext(), animationConfig.getLottieConfig());
if (resourceEntry != null) {
InputStream inputRaw =
resourceEntry.getResources().openRawResource(resourceEntry.getResourceId());
lottieView.setAnimation(inputRaw, null);
lottieView.playAnimation();
setLottieLayoutVisibility(View.VISIBLE);
setIllustrationLayoutVisibility(View.GONE);
applyThemeCustomization();
} else {
setLottieLayoutVisibility(View.GONE);
setIllustrationLayoutVisibility(View.VISIBLE);
inflateIllustrationStub();
}
}
}
private void setIllustrationLayoutVisibility(int visibility) {
View illustrationLayout = findViewById(R.id.sud_layout_progress_illustration);
if (illustrationLayout != null) {
illustrationLayout.setVisibility(visibility);
}
}
private void setLottieLayoutVisibility(int visibility) {
View lottieLayout = findViewById(R.id.sud_layout_lottie_illustration);
if (lottieLayout != null) {
lottieLayout.setVisibility(visibility);
}
}
@VisibleForTesting
boolean isLottieLayoutVisible() {
View lottieLayout = findViewById(R.id.sud_layout_lottie_illustration);
return lottieLayout != null && lottieLayout.getVisibility() == View.VISIBLE;
}
private void setIllustrationResource() {
View illustrationLayout = findViewById(R.id.sud_layout_progress_illustration);
if (illustrationLayout == null) {
Log.i(TAG, "Illustration stub not inflated, skip set resource");
return;
}
IllustrationVideoView illustrationVideoView =
findManagedViewById(R.id.sud_progress_illustration);
ProgressBar progressBar = findManagedViewById(R.id.sud_progress_bar);
PartnerConfigHelper partnerConfigHelper = PartnerConfigHelper.get(getContext());
ResourceEntry resourceEntry =
partnerConfigHelper.getIllustrationResourceEntry(
getContext(), animationConfig.getIllustrationConfig());
if (resourceEntry != null) {
progressBar.setVisibility(GONE);
illustrationVideoView.setVisibility(VISIBLE);
illustrationVideoView.setVideoResourceEntry(resourceEntry);
} else {
progressBar.setVisibility(VISIBLE);
illustrationVideoView.setVisibility(GONE);
}
}
private LottieAnimationView findLottieAnimationView() {
return findViewById(R.id.sud_lottie_view);
}
private IllustrationVideoView findIllustrationVideoView() {
return findManagedViewById(R.id.sud_progress_illustration);
}
public void playAnimation() {
LottieAnimationView lottieAnimationView = findLottieAnimationView();
if (lottieAnimationView != null) {
lottieAnimationView.setRepeatCount(LottieDrawable.INFINITE);
lottieAnimationView.playAnimation();
}
}
/** Returns whether the layout is waiting for animation finish or not. */
public boolean isFinishing() {
LottieAnimationView lottieAnimationView = findLottieAnimationView();
if (lottieAnimationView != null) {
return !animationFinishListeners.isEmpty() && lottieAnimationView.getRepeatCount() == 0;
} else {
return false;
}
}
@AnimationType
public int getAnimationType() {
if (findLottieAnimationView() != null && isLottieLayoutVisible()) {
return AnimationType.LOTTIE;
} else if (findIllustrationVideoView() != null) {
return AnimationType.ILLUSTRATION;
} else {
return AnimationType.PROGRESS_BAR;
}
}
// TODO: Should add testcase with mocked LottieAnimationView.
/** Add an animator listener to {@link LottieAnimationView}. */
public void addAnimatorListener(Animator.AnimatorListener listener) {
LottieAnimationView animationView = findLottieAnimationView();
if (animationView != null) {
animationView.addAnimatorListener(listener);
}
}
/** Remove the listener from {@link LottieAnimationView}. */
public void removeAnimatorListener(AnimatorListener listener) {
LottieAnimationView animationView = findLottieAnimationView();
if (animationView != null) {
animationView.removeAnimatorListener(listener);
}
}
/** Remove all {@link AnimatorListener} from {@link LottieAnimationView}. */
public void removeAllAnimatorListener() {
LottieAnimationView animationView = findLottieAnimationView();
if (animationView != null) {
animationView.removeAllAnimatorListeners();
}
}
/** Add a value callback with property {@link LottieProperty.COLOR_FILTER}. */
public void addColorCallback(KeyPath keyPath, LottieValueCallback<ColorFilter> callback) {
LottieAnimationView animationView = findLottieAnimationView();
if (animationView != null) {
animationView.addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback);
}
}
/** Add a simple value callback with property {@link LottieProperty.COLOR_FILTER}. */
public void addColorCallback(KeyPath keyPath, SimpleLottieValueCallback<ColorFilter> callback) {
LottieAnimationView animationView = findLottieAnimationView();
if (animationView != null) {
animationView.addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback);
}
}
@VisibleForTesting
protected void loadCustomization() {
if (customizationMap.isEmpty()) {
PartnerConfigHelper helper = PartnerConfigHelper.get(getContext());
List<String> lists =
helper.getStringArray(
getContext(),
isNightMode(getResources().getConfiguration())
? animationConfig.getDarkThemeCustomization()
: animationConfig.getLightThemeCustomization());
for (String item : lists) {
String[] splitItem = item.split(":");
if (splitItem.length == 2) {
customizationMap.put(
new KeyPath(splitItem[0]), new SimpleColorFilter(Color.parseColor(splitItem[1])));
} else {
Log.w(TAG, "incorrect format customization, value=" + item);
}
}
}
}
@VisibleForTesting
protected void applyThemeCustomization() {
LottieAnimationView animationView = findLottieAnimationView();
if (animationView != null) {
loadCustomization();
for (KeyPath keyPath : customizationMap.keySet()) {
animationView.addValueCallback(
keyPath,
LottieProperty.COLOR_FILTER,
new LottieValueCallback<>(customizationMap.get(keyPath)));
}
}
}
@Nullable
private View peekLottieLayout() {
return findViewById(R.id.sud_layout_lottie_illustration);
}
@Nullable
private View peekProgressIllustrationLayout() {
return findViewById(R.id.sud_layout_progress_illustration);
}
@Override
protected View onInflateTemplate(LayoutInflater inflater, int template) {
if (template == 0) {
template = R.layout.sud_glif_loading_template;
}
return inflateTemplate(inflater, R.style.SudThemeGlif_Light, template);
}
@Override
protected ViewGroup findContainer(int containerId) {
if (containerId == 0) {
containerId = R.id.sud_layout_content;
}
return super.findContainer(containerId);
}
/** The progress config used to maps to different animation */
public enum LottieAnimationConfig {
CONFIG_DEFAULT(
PartnerConfig.CONFIG_PROGRESS_ILLUSTRATION_DEFAULT,
PartnerConfig.CONFIG_LOADING_LOTTIE_DEFAULT,
PartnerConfig.CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_DEFAULT,
PartnerConfig.CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_DEFAULT),
CONFIG_ACCOUNT(
PartnerConfig.CONFIG_PROGRESS_ILLUSTRATION_ACCOUNT,
PartnerConfig.CONFIG_LOADING_LOTTIE_ACCOUNT,
PartnerConfig.CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_ACCOUNT,
PartnerConfig.CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_ACCOUNT),
CONFIG_CONNECTION(
PartnerConfig.CONFIG_PROGRESS_ILLUSTRATION_CONNECTION,
PartnerConfig.CONFIG_LOADING_LOTTIE_CONNECTION,
PartnerConfig.CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_CONNECTION,
PartnerConfig.CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_CONNECTION),
CONFIG_UPDATE(
PartnerConfig.CONFIG_PROGRESS_ILLUSTRATION_UPDATE,
PartnerConfig.CONFIG_LOADING_LOTTIE_UPDATE,
PartnerConfig.CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_UPDATE,
PartnerConfig.CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_UPDATE),
CONFIG_FINAL_HOLD(
PartnerConfig.CONFIG_PROGRESS_ILLUSTRATION_FINAL_HOLD,
PartnerConfig.CONFIG_LOADING_LOTTIE_FINAL_HOLD,
PartnerConfig.CONFIG_LOTTIE_LIGHT_THEME_CUSTOMIZATION_FINAL_HOLD,
PartnerConfig.CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_FINAL_HOLD);
private final PartnerConfig illustrationConfig;
private final PartnerConfig lottieConfig;
private final PartnerConfig lightThemeCustomization;
private final PartnerConfig darkThemeCustomization;
LottieAnimationConfig(
PartnerConfig illustrationConfig,
PartnerConfig lottieConfig,
PartnerConfig lightThemeCustomization,
PartnerConfig darkThemeCustomization) {
if (illustrationConfig.getResourceType() != ResourceType.ILLUSTRATION
|| lottieConfig.getResourceType() != ResourceType.ILLUSTRATION) {
throw new IllegalArgumentException(
"Illustration progress only allow illustration resource");
}
this.illustrationConfig = illustrationConfig;
this.lottieConfig = lottieConfig;
this.lightThemeCustomization = lightThemeCustomization;
this.darkThemeCustomization = darkThemeCustomization;
}
PartnerConfig getIllustrationConfig() {
return illustrationConfig;
}
PartnerConfig getLottieConfig() {
return lottieConfig;
}
PartnerConfig getLightThemeCustomization() {
return lightThemeCustomization;
}
PartnerConfig getDarkThemeCustomization() {
return darkThemeCustomization;
}
}
/**
* Register the {@link Runnable} as a callback class that will be perform when animation finished.
*/
public void registerAnimationFinishRunnable(Runnable runnable) {
registerAnimationFinishRunnable(runnable, /* allowFinishWithMaximumDuration= */ false);
}
/**
* Register the {@link Runnable} as a callback class that will be perform when animation finished.
* {@code allowFinishWithMaximumDuration} to allow the animation finish advanced by {@link
* PartnerConfig#CONFIG_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS} config. The {@code runnable}
* will be performed if the Lottie animation finish played and the duration of Lottie animation
* less than @link PartnerConfig#CONFIG_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS} config.
*/
public void registerAnimationFinishRunnable(
Runnable runnable, boolean allowFinishWithMaximumDuration) {
if (allowFinishWithMaximumDuration) {
int delayMs =
PartnerConfigHelper.get(getContext())
.getInteger(
getContext(), PartnerConfig.CONFIG_PROGRESS_ILLUSTRATION_DISPLAY_MINIMUM_MS, 0);
animationFinishListeners.add(new LottieAnimationFinishListener(this, runnable, delayMs));
} else {
animationFinishListeners.add(
new LottieAnimationFinishListener(this, runnable, /* finishWithMinimumDuration= */ 0L));
}
}
/** The listener that to indicate the playing status for lottie animation. */
@VisibleForTesting
public static class LottieAnimationFinishListener {
private final Handler handler;
private final Runnable runnable;
private final GlifLoadingLayout glifLoadingLayout;
private final LottieAnimationView lottieAnimationView;
@VisibleForTesting
AnimatorListener animatorListener =
new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// Do nothing.
}
@Override
public void onAnimationEnd(Animator animation) {
onAnimationFinished();
}
@Override
public void onAnimationCancel(Animator animation) {
// Do nothing.
}
@Override
public void onAnimationRepeat(Animator animation) {
// Do nothing.
}
};
@VisibleForTesting
LottieAnimationFinishListener(
GlifLoadingLayout glifLoadingLayout, Runnable runnable, long finishWithMinimumDuration) {
if (runnable == null) {
throw new NullPointerException("Runnable can not be null");
}
this.glifLoadingLayout = glifLoadingLayout;
this.runnable = runnable;
this.handler = new Handler(Looper.getMainLooper());
this.lottieAnimationView = glifLoadingLayout.findLottieAnimationView();
if (glifLoadingLayout.isLottieLayoutVisible() && !isZeroAnimatorDurationScale()) {
lottieAnimationView.setRepeatCount(0);
lottieAnimationView.addAnimatorListener(animatorListener);
if (finishWithMinimumDuration > 0) {
handler.postDelayed(this::onAnimationFinished, finishWithMinimumDuration);
}
} else {
onAnimationFinished();
}
}
@VisibleForTesting
boolean isZeroAnimatorDurationScale() {
try {
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
return Global.getFloat(
glifLoadingLayout.getContext().getContentResolver(), Global.ANIMATOR_DURATION_SCALE)
== 0f;
} else {
return false;
}
} catch (SettingNotFoundException e) {
return false;
}
}
@VisibleForTesting
public void onAnimationFinished() {
handler.removeCallbacks(runnable);
runnable.run();
if (lottieAnimationView != null) {
lottieAnimationView.removeAnimatorListener(animatorListener);
}
glifLoadingLayout.animationFinishListeners.remove(this);
}
}
/** Annotates the state for the illustration. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
IllustrationType.ACCOUNT,
IllustrationType.CONNECTION,
IllustrationType.DEFAULT,
IllustrationType.UPDATE,
IllustrationType.FINAL_HOLD
})
public @interface IllustrationType {
String DEFAULT = "default";
String ACCOUNT = "account";
String CONNECTION = "connection";
String UPDATE = "update";
String FINAL_HOLD = "final_hold";
}
/** Annotates the type for the illustration. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({AnimationType.LOTTIE, AnimationType.ILLUSTRATION, AnimationType.PROGRESS_BAR})
public @interface AnimationType {
int LOTTIE = 1;
int ILLUSTRATION = 2;
int PROGRESS_BAR = 3;
}
}