blob: 2e5d859774381d7de48896bbd5a143529ddc4d9f [file] [log] [blame]
/*
* Copyright 2018 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 androidx.wear.widget;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import androidx.wear.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
/**
* Displays a full-screen confirmation animation with optional text and then hides it.
*
* <p>This is a lighter-weight version of {@link androidx.wear.activity.ConfirmationActivity}
* and should be preferred when constructed from an {@link Activity}.
*
* <p>Sample usage:
*
* <pre>
* // Defaults to SUCCESS_ANIMATION
* new ConfirmationOverlay().showOn(myActivity);
*
* new ConfirmationOverlay()
* .setType(ConfirmationOverlay.OPEN_ON_PHONE_ANIMATION)
* .setDuration(3000)
* .setMessage("Opening...")
* .setFinishedAnimationListener(new ConfirmationOverlay.OnAnimationFinishedListener() {
* {@literal @}Override
* public void onAnimationFinished() {
* // Finished animating and the content view has been removed from myActivity.
* }
* }).showOn(myActivity);
*
* // Default duration is {@link #DEFAULT_ANIMATION_DURATION_MS}
* new ConfirmationOverlay()
* .setType(ConfirmationOverlay.FAILURE_ANIMATION)
* .setMessage("Failed")
* .setFinishedAnimationListener(new ConfirmationOverlay.OnAnimationFinishedListener() {
* {@literal @}Override
* public void onAnimationFinished() {
* // Finished animating and the view has been removed from myView.getRootView().
* }
* }).showAbove(myView);
* </pre>
*/
public class ConfirmationOverlay {
/**
* Interface for listeners to be notified when the {@link ConfirmationOverlay} animation has
* finished and its {@link View} has been removed.
*/
public interface OnAnimationFinishedListener {
/**
* Called when the confirmation animation is finished.
*/
void onAnimationFinished();
}
/** Default animation duration in ms. **/
public static final int DEFAULT_ANIMATION_DURATION_MS = 1000;
/** Types of animations to display in the overlay. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({SUCCESS_ANIMATION, FAILURE_ANIMATION, OPEN_ON_PHONE_ANIMATION})
public @interface OverlayType {
}
/** {@link OverlayType} indicating the success animation overlay should be displayed. */
public static final int SUCCESS_ANIMATION = 0;
/**
* {@link OverlayType} indicating the failure overlay should be shown. The icon associated with
* this type, unlike the others, does not animate.
*/
public static final int FAILURE_ANIMATION = 1;
/** {@link OverlayType} indicating the "Open on Phone" animation overlay should be displayed. */
public static final int OPEN_ON_PHONE_ANIMATION = 2;
@OverlayType
private int mType = SUCCESS_ANIMATION;
private int mDurationMillis = DEFAULT_ANIMATION_DURATION_MS;
private OnAnimationFinishedListener mListener;
private String mMessage;
private View mOverlayView;
private Drawable mOverlayDrawable;
private boolean mIsShowing = false;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final Runnable mHideRunnable =
new Runnable() {
@Override
public void run() {
hide();
}
};
/**
* Sets a message which will be displayed at the same time as the animation.
*
* @return {@code this} object for method chaining.
*/
public ConfirmationOverlay setMessage(String message) {
mMessage = message;
return this;
}
/**
* Sets the {@link OverlayType} which controls which animation is displayed.
*
* @return {@code this} object for method chaining.
*/
public ConfirmationOverlay setType(@OverlayType int type) {
mType = type;
return this;
}
/**
* Sets the duration in milliseconds which controls how long the animation will be displayed.
* Default duration is {@link #DEFAULT_ANIMATION_DURATION_MS}.
*
* @return {@code this} object for method chaining.
*/
public ConfirmationOverlay setDuration(int millis) {
mDurationMillis = millis;
return this;
}
/**
* Sets the {@link OnAnimationFinishedListener} which will be invoked once the overlay is no
* longer visible.
*
* @return {@code this} object for method chaining.
*/
public ConfirmationOverlay setFinishedAnimationListener(
@Nullable OnAnimationFinishedListener listener) {
mListener = listener;
return this;
}
/**
* Adds the overlay as a child of {@code view.getRootView()}, removing it when complete. While
* it is shown, all touches will be intercepted to prevent accidental taps on obscured views.
*/
@MainThread
public void showAbove(View view) {
if (mIsShowing) {
return;
}
mIsShowing = true;
updateOverlayView(view.getContext());
((ViewGroup) view.getRootView()).addView(mOverlayView);
animateAndHideAfterDelay();
}
/**
* Adds the overlay as a content view to the {@code activity}, removing it when complete. While
* it is shown, all touches will be intercepted to prevent accidental taps on obscured views.
*/
@MainThread
public void showOn(Activity activity) {
if (mIsShowing) {
return;
}
mIsShowing = true;
updateOverlayView(activity);
activity.getWindow().addContentView(mOverlayView, mOverlayView.getLayoutParams());
animateAndHideAfterDelay();
}
@MainThread
private void animateAndHideAfterDelay() {
if (mOverlayDrawable instanceof Animatable) {
Animatable animatable = (Animatable) mOverlayDrawable;
animatable.start();
}
mMainThreadHandler.postDelayed(mHideRunnable, mDurationMillis);
}
/**
* Starts a fadeout animation and removes the view once finished. This is invoked by {@link
* #mHideRunnable} after {@link #mDurationMillis} milliseconds.
*
* @hide
*/
@MainThread
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void hide() {
Animation fadeOut =
AnimationUtils.loadAnimation(mOverlayView.getContext(), android.R.anim.fade_out);
fadeOut.setAnimationListener(
new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mOverlayView.clearAnimation();
}
@Override
public void onAnimationEnd(Animation animation) {
((ViewGroup) mOverlayView.getParent()).removeView(mOverlayView);
mIsShowing = false;
if (mListener != null) {
mListener.onAnimationFinished();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
mOverlayView.startAnimation(fadeOut);
}
@MainThread
private void updateOverlayView(Context context) {
if (mOverlayView == null) {
//noinspection InflateParams
mOverlayView =
LayoutInflater.from(context).inflate(R.layout.ws_overlay_confirmation, null);
}
mOverlayView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
mOverlayView.setLayoutParams(
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
updateImageView(context, mOverlayView);
updateMessageView(context, mOverlayView);
}
@MainThread
private void updateMessageView(Context context, View overlayView) {
TextView messageView =
overlayView.findViewById(R.id.wearable_support_confirmation_overlay_message);
if (mMessage != null) {
int screenWidthPx = ResourcesUtil.getScreenWidthPx(context);
int topMarginPx = ResourcesUtil.getFractionOfScreenPx(
context, screenWidthPx, R.fraction.confirmation_overlay_margin_above_text);
int sideMarginPx =
ResourcesUtil.getFractionOfScreenPx(
context, screenWidthPx, R.fraction.confirmation_overlay_margin_side);
MarginLayoutParams layoutParams = (MarginLayoutParams) messageView.getLayoutParams();
layoutParams.topMargin = topMarginPx;
layoutParams.leftMargin = sideMarginPx;
layoutParams.rightMargin = sideMarginPx;
messageView.setLayoutParams(layoutParams);
messageView.setText(mMessage);
messageView.setVisibility(View.VISIBLE);
} else {
messageView.setVisibility(View.GONE);
}
}
@MainThread
private void updateImageView(Context context, View overlayView) {
switch (mType) {
case SUCCESS_ANIMATION:
mOverlayDrawable = ContextCompat.getDrawable(context,
R.drawable.generic_confirmation_animation);
break;
case FAILURE_ANIMATION:
mOverlayDrawable = ContextCompat.getDrawable(context, R.drawable.ws_full_sad);
break;
case OPEN_ON_PHONE_ANIMATION:
mOverlayDrawable =
ContextCompat.getDrawable(context, R.drawable.ws_open_on_phone_animation);
break;
default:
String errorMessage =
String.format(Locale.US, "Invalid ConfirmationOverlay type [%d]", mType);
throw new IllegalStateException(errorMessage);
}
ImageView imageView =
overlayView.findViewById(R.id.wearable_support_confirmation_overlay_image);
imageView.setImageDrawable(mOverlayDrawable);
}
}