Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright 2018 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 androidx.wear.widget;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.graphics.drawable.Animatable;
     22 import android.graphics.drawable.Drawable;
     23 import android.os.Handler;
     24 import android.os.Looper;
     25 import android.view.LayoutInflater;
     26 import android.view.MotionEvent;
     27 import android.view.View;
     28 import android.view.View.OnTouchListener;
     29 import android.view.ViewGroup;
     30 import android.view.ViewGroup.LayoutParams;
     31 import android.view.ViewGroup.MarginLayoutParams;
     32 import android.view.animation.Animation;
     33 import android.view.animation.Animation.AnimationListener;
     34 import android.view.animation.AnimationUtils;
     35 import android.widget.ImageView;
     36 import android.widget.TextView;
     37 
     38 import androidx.annotation.IntDef;
     39 import androidx.annotation.MainThread;
     40 import androidx.annotation.Nullable;
     41 import androidx.annotation.RestrictTo;
     42 import androidx.annotation.VisibleForTesting;
     43 import androidx.core.content.ContextCompat;
     44 import androidx.wear.R;
     45 
     46 import java.lang.annotation.Retention;
     47 import java.lang.annotation.RetentionPolicy;
     48 import java.util.Locale;
     49 
     50 /**
     51  * Displays a full-screen confirmation animation with optional text and then hides it.
     52  *
     53  * <p>This is a lighter-weight version of {@link androidx.wear.activity.ConfirmationActivity}
     54  * and should be preferred when constructed from an {@link Activity}.
     55  *
     56  * <p>Sample usage:
     57  *
     58  * <pre>
     59  *   // Defaults to SUCCESS_ANIMATION
     60  *   new ConfirmationOverlay().showOn(myActivity);
     61  *
     62  *   new ConfirmationOverlay()
     63  *      .setType(ConfirmationOverlay.OPEN_ON_PHONE_ANIMATION)
     64  *      .setDuration(3000)
     65  *      .setMessage("Opening...")
     66  *      .setFinishedAnimationListener(new ConfirmationOverlay.OnAnimationFinishedListener() {
     67  *          {@literal @}Override
     68  *          public void onAnimationFinished() {
     69  *              // Finished animating and the content view has been removed from myActivity.
     70  *          }
     71  *      }).showOn(myActivity);
     72  *
     73  *   // Default duration is {@link #DEFAULT_ANIMATION_DURATION_MS}
     74  *   new ConfirmationOverlay()
     75  *      .setType(ConfirmationOverlay.FAILURE_ANIMATION)
     76  *      .setMessage("Failed")
     77  *      .setFinishedAnimationListener(new ConfirmationOverlay.OnAnimationFinishedListener() {
     78  *          {@literal @}Override
     79  *          public void onAnimationFinished() {
     80  *              // Finished animating and the view has been removed from myView.getRootView().
     81  *          }
     82  *      }).showAbove(myView);
     83  * </pre>
     84  */
     85 public class ConfirmationOverlay {
     86 
     87     /**
     88      * Interface for listeners to be notified when the {@link ConfirmationOverlay} animation has
     89      * finished and its {@link View} has been removed.
     90      */
     91     public interface OnAnimationFinishedListener {
     92         /**
     93          * Called when the confirmation animation is finished.
     94          */
     95         void onAnimationFinished();
     96     }
     97 
     98     /** Default animation duration in ms. **/
     99     public static final int DEFAULT_ANIMATION_DURATION_MS = 1000;
    100 
    101     /** Types of animations to display in the overlay. */
    102     @Retention(RetentionPolicy.SOURCE)
    103     @IntDef({SUCCESS_ANIMATION, FAILURE_ANIMATION, OPEN_ON_PHONE_ANIMATION})
    104     public @interface OverlayType {
    105     }
    106 
    107     /** {@link OverlayType} indicating the success animation overlay should be displayed. */
    108     public static final int SUCCESS_ANIMATION = 0;
    109 
    110     /**
    111      * {@link OverlayType} indicating the failure overlay should be shown. The icon associated with
    112      * this type, unlike the others, does not animate.
    113      */
    114     public static final int FAILURE_ANIMATION = 1;
    115 
    116     /** {@link OverlayType} indicating the "Open on Phone" animation overlay should be displayed. */
    117     public static final int OPEN_ON_PHONE_ANIMATION = 2;
    118 
    119     @OverlayType
    120     private int mType = SUCCESS_ANIMATION;
    121     private int mDurationMillis = DEFAULT_ANIMATION_DURATION_MS;
    122     private OnAnimationFinishedListener mListener;
    123     private String mMessage;
    124     private View mOverlayView;
    125     private Drawable mOverlayDrawable;
    126     private boolean mIsShowing = false;
    127 
    128     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
    129     private final Runnable mHideRunnable =
    130             new Runnable() {
    131                 @Override
    132                 public void run() {
    133                     hide();
    134                 }
    135             };
    136 
    137     /**
    138      * Sets a message which will be displayed at the same time as the animation.
    139      *
    140      * @return {@code this} object for method chaining.
    141      */
    142     public ConfirmationOverlay setMessage(String message) {
    143         mMessage = message;
    144         return this;
    145     }
    146 
    147     /**
    148      * Sets the {@link OverlayType} which controls which animation is displayed.
    149      *
    150      * @return {@code this} object for method chaining.
    151      */
    152     public ConfirmationOverlay setType(@OverlayType int type) {
    153         mType = type;
    154         return this;
    155     }
    156 
    157     /**
    158      * Sets the duration in milliseconds which controls how long the animation will be displayed.
    159      * Default duration is {@link #DEFAULT_ANIMATION_DURATION_MS}.
    160      *
    161      * @return {@code this} object for method chaining.
    162      */
    163     public ConfirmationOverlay setDuration(int millis) {
    164         mDurationMillis = millis;
    165         return this;
    166     }
    167 
    168     /**
    169      * Sets the {@link OnAnimationFinishedListener} which will be invoked once the overlay is no
    170      * longer visible.
    171      *
    172      * @return {@code this} object for method chaining.
    173      */
    174     public ConfirmationOverlay setFinishedAnimationListener(
    175             @Nullable OnAnimationFinishedListener listener) {
    176         mListener = listener;
    177         return this;
    178     }
    179 
    180     /**
    181      * Adds the overlay as a child of {@code view.getRootView()}, removing it when complete. While
    182      * it is shown, all touches will be intercepted to prevent accidental taps on obscured views.
    183      */
    184     @MainThread
    185     public void showAbove(View view) {
    186         if (mIsShowing) {
    187             return;
    188         }
    189         mIsShowing = true;
    190 
    191         updateOverlayView(view.getContext());
    192         ((ViewGroup) view.getRootView()).addView(mOverlayView);
    193         animateAndHideAfterDelay();
    194     }
    195 
    196     /**
    197      * Adds the overlay as a content view to the {@code activity}, removing it when complete. While
    198      * it is shown, all touches will be intercepted to prevent accidental taps on obscured views.
    199      */
    200     @MainThread
    201     public void showOn(Activity activity) {
    202         if (mIsShowing) {
    203             return;
    204         }
    205         mIsShowing = true;
    206 
    207         updateOverlayView(activity);
    208         activity.getWindow().addContentView(mOverlayView, mOverlayView.getLayoutParams());
    209         animateAndHideAfterDelay();
    210     }
    211 
    212     @MainThread
    213     private void animateAndHideAfterDelay() {
    214         if (mOverlayDrawable instanceof Animatable) {
    215             Animatable animatable = (Animatable) mOverlayDrawable;
    216             animatable.start();
    217         }
    218         mMainThreadHandler.postDelayed(mHideRunnable, mDurationMillis);
    219     }
    220 
    221     /**
    222      * Starts a fadeout animation and removes the view once finished. This is invoked by {@link
    223      * #mHideRunnable} after {@link #mDurationMillis} milliseconds.
    224      *
    225      * @hide
    226      */
    227     @MainThread
    228     @VisibleForTesting
    229     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    230     public void hide() {
    231         Animation fadeOut =
    232                 AnimationUtils.loadAnimation(mOverlayView.getContext(), android.R.anim.fade_out);
    233         fadeOut.setAnimationListener(
    234                 new AnimationListener() {
    235                     @Override
    236                     public void onAnimationStart(Animation animation) {
    237                         mOverlayView.clearAnimation();
    238                     }
    239 
    240                     @Override
    241                     public void onAnimationEnd(Animation animation) {
    242                         ((ViewGroup) mOverlayView.getParent()).removeView(mOverlayView);
    243                         mIsShowing = false;
    244                         if (mListener != null) {
    245                             mListener.onAnimationFinished();
    246                         }
    247                     }
    248 
    249                     @Override
    250                     public void onAnimationRepeat(Animation animation) {
    251                     }
    252                 });
    253         mOverlayView.startAnimation(fadeOut);
    254     }
    255 
    256     @MainThread
    257     private void updateOverlayView(Context context) {
    258         if (mOverlayView == null) {
    259             //noinspection InflateParams
    260             mOverlayView =
    261                     LayoutInflater.from(context).inflate(R.layout.ws_overlay_confirmation, null);
    262         }
    263         mOverlayView.setOnTouchListener(new OnTouchListener() {
    264             @Override
    265             public boolean onTouch(View v, MotionEvent event) {
    266                 return true;
    267             }
    268         });
    269         mOverlayView.setLayoutParams(
    270                 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    271 
    272         updateImageView(context, mOverlayView);
    273         updateMessageView(context, mOverlayView);
    274     }
    275 
    276     @MainThread
    277     private void updateMessageView(Context context, View overlayView) {
    278         TextView messageView =
    279                 overlayView.findViewById(R.id.wearable_support_confirmation_overlay_message);
    280 
    281         if (mMessage != null) {
    282             int screenWidthPx = ResourcesUtil.getScreenWidthPx(context);
    283             int topMarginPx = ResourcesUtil.getFractionOfScreenPx(
    284                     context, screenWidthPx, R.fraction.confirmation_overlay_margin_above_text);
    285             int sideMarginPx =
    286                     ResourcesUtil.getFractionOfScreenPx(
    287                             context, screenWidthPx, R.fraction.confirmation_overlay_margin_side);
    288 
    289             MarginLayoutParams layoutParams = (MarginLayoutParams) messageView.getLayoutParams();
    290             layoutParams.topMargin = topMarginPx;
    291             layoutParams.leftMargin = sideMarginPx;
    292             layoutParams.rightMargin = sideMarginPx;
    293 
    294             messageView.setLayoutParams(layoutParams);
    295             messageView.setText(mMessage);
    296             messageView.setVisibility(View.VISIBLE);
    297 
    298         } else {
    299             messageView.setVisibility(View.GONE);
    300         }
    301     }
    302 
    303     @MainThread
    304     private void updateImageView(Context context, View overlayView) {
    305         switch (mType) {
    306             case SUCCESS_ANIMATION:
    307                 mOverlayDrawable = ContextCompat.getDrawable(context,
    308                         R.drawable.generic_confirmation_animation);
    309                 break;
    310             case FAILURE_ANIMATION:
    311                 mOverlayDrawable = ContextCompat.getDrawable(context, R.drawable.ws_full_sad);
    312                 break;
    313             case OPEN_ON_PHONE_ANIMATION:
    314                 mOverlayDrawable =
    315                         ContextCompat.getDrawable(context, R.drawable.ws_open_on_phone_animation);
    316                 break;
    317             default:
    318                 String errorMessage =
    319                         String.format(Locale.US, "Invalid ConfirmationOverlay type [%d]", mType);
    320                 throw new IllegalStateException(errorMessage);
    321         }
    322 
    323         ImageView imageView =
    324                 overlayView.findViewById(R.id.wearable_support_confirmation_overlay_image);
    325         imageView.setImageDrawable(mOverlayDrawable);
    326     }
    327 }
    328