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