ConfirmationOverlay.java revision e09a39f7a9448990d4358e5255f6ef93da860901
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 17package androidx.wear.widget; 18 19import android.app.Activity; 20import android.content.Context; 21import android.graphics.drawable.Animatable; 22import android.graphics.drawable.Drawable; 23import android.os.Handler; 24import android.os.Looper; 25import android.view.LayoutInflater; 26import android.view.MotionEvent; 27import android.view.View; 28import android.view.View.OnTouchListener; 29import android.view.ViewGroup; 30import android.view.ViewGroup.LayoutParams; 31import android.view.ViewGroup.MarginLayoutParams; 32import android.view.animation.Animation; 33import android.view.animation.Animation.AnimationListener; 34import android.view.animation.AnimationUtils; 35import android.widget.ImageView; 36import android.widget.TextView; 37 38import java.lang.annotation.Retention; 39import java.lang.annotation.RetentionPolicy; 40import java.util.Locale; 41 42import androidx.annotation.IntDef; 43import androidx.annotation.MainThread; 44import androidx.annotation.Nullable; 45import androidx.annotation.RestrictTo; 46import androidx.annotation.VisibleForTesting; 47import androidx.core.content.ContextCompat; 48import androidx.wear.R; 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 */ 85public 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