1 /*
2  * Copyright (C) 2017 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 package com.android.wallpaper.picker.individual;
17 
18 import android.animation.Animator;
19 import android.animation.Animator.AnimatorListener;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.content.Context;
23 import android.os.Handler;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.View.OnHoverListener;
27 import android.widget.ImageView;
28 
29 import com.android.wallpaper.R;
30 
31 /**
32  * Implementation of {@code SelectionAnimator} which uses a checkmark and inset around the tile to
33  * indicate a selected state.
34  */
35 public class CheckmarkSelectionAnimator implements SelectionAnimator {
36     private static final int HOVER_TIMEOUT_MS = 200;
37     private static final float HOVER_CHECK_CIRCLE_OPACITY = 0.67f;
38 
39     private Context mAppContext;
40 
41     private View mTile;
42     private ImageView mCheckCircle;
43     private View mLoadingIndicatorContainer;
44     private boolean mIsSelected;
45     private boolean mIsHovered;
46     private Handler mHoverHandler;
47 
48     private Runnable mHoverEnterRunnable = new Runnable() {
49         @Override
50         public void run() {
51             mIsHovered = true;
52 
53             mCheckCircle.setImageDrawable(mAppContext.getDrawable(
54                     R.drawable.material_ic_check_circle_white_24));
55             mCheckCircle.setVisibility(View.VISIBLE);
56             ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(
57                     mCheckCircle, "alpha", 0f, HOVER_CHECK_CIRCLE_OPACITY);
58             alphaAnimator.start();
59 
60             alphaAnimator.addListener(new AnimatorListener() {
61                 @Override
62                 public void onAnimationStart(Animator animation) {
63                 }
64 
65                 @Override
66                 public void onAnimationEnd(Animator animation) {
67                     mIsHovered = true;
68                 }
69 
70                 @Override
71                 public void onAnimationCancel(Animator animation) {
72                 }
73 
74                 @Override
75                 public void onAnimationRepeat(Animator animation) {
76                 }
77             });
78         }
79     };
80 
81     private Runnable mHoverExitRunnable = new Runnable() {
82         @Override
83         public void run() {
84             mIsHovered = false;
85 
86             ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(
87                     mCheckCircle, "alpha", HOVER_CHECK_CIRCLE_OPACITY, 0f);
88             alphaAnimator.addListener(new AnimatorListener() {
89                 @Override
90                 public void onAnimationStart(Animator animation) {
91                 }
92 
93                 @Override
94                 public void onAnimationEnd(Animator animation) {
95                     mCheckCircle.setVisibility(View.GONE);
96                     mIsHovered = false;
97                 }
98 
99                 @Override
100                 public void onAnimationCancel(Animator animation) {
101                 }
102 
103                 @Override
104                 public void onAnimationRepeat(Animator animation) {
105                 }
106             });
107 
108             alphaAnimator.start();
109         }
110     };
111 
CheckmarkSelectionAnimator(Context appContext, View itemView)112     public CheckmarkSelectionAnimator(Context appContext, View itemView) {
113         mAppContext = appContext;
114 
115         mTile = itemView.findViewById(R.id.tile);
116         mCheckCircle = (ImageView) itemView.findViewById(R.id.check_circle);
117         mLoadingIndicatorContainer = itemView.findViewById(R.id.loading_indicator_container);
118         mHoverHandler = new Handler();
119 
120         mTile.setOnHoverListener(new OnHoverListener() {
121             @Override
122             public boolean onHover(View v, MotionEvent event) {
123                 // If this ViewHolder is already selected, then don't change the state of the check circle.
124                 if (mIsSelected) {
125                     return false;
126                 }
127 
128                 int actionMasked = event.getActionMasked();
129 
130                 switch (actionMasked) {
131                     case MotionEvent.ACTION_HOVER_ENTER:
132                         animateHoverEnter();
133                         break;
134                     case MotionEvent.ACTION_HOVER_EXIT:
135                         animateHoverExit();
136                         break;
137                     default:
138                         // fall out
139                 }
140 
141                 return false;
142             }
143         });
144     }
145 
146     @Override
selectImmediately()147     public void selectImmediately() {
148         mIsSelected = true;
149         int insetPx = mAppContext.getResources().getDimensionPixelSize(
150                 R.dimen.grid_item_individual_wallpaper_selected_inset);
151         mTile.setPadding(insetPx, insetPx, insetPx, insetPx);
152         mCheckCircle.setImageDrawable(mAppContext.getDrawable(R.drawable.check_circle_blue));
153         mCheckCircle.setVisibility(View.VISIBLE);
154         mCheckCircle.setAlpha(1f);
155         mLoadingIndicatorContainer.setVisibility(View.GONE);
156     }
157 
158     @Override
deselectImmediately()159     public void deselectImmediately() {
160         mIsSelected = false;
161         mCheckCircle.setAlpha(0f);
162         mCheckCircle.setVisibility(View.GONE);
163         mTile.setPadding(0, 0, 0, 0);
164         mLoadingIndicatorContainer.setVisibility(View.GONE);
165     }
166 
167     @Override
animateSelected()168     public void animateSelected() {
169         // If already selected, do nothing.
170         if (mIsSelected) {
171             return;
172         }
173 
174         mLoadingIndicatorContainer.setVisibility(View.GONE);
175 
176         int[][] values = new int[2][4];
177         values[0] = new int[]{0, 0, 0, 0};
178         int insetPx = mAppContext.getResources().getDimensionPixelSize(
179                 R.dimen.grid_item_individual_wallpaper_selected_inset);
180         values[1] = new int[]{insetPx, insetPx, insetPx, insetPx};
181 
182         ObjectAnimator paddingAnimator = ObjectAnimator.ofMultiInt(mTile, "padding", values);
183         ObjectAnimator checkCircleAlphaAnimator = ObjectAnimator.ofFloat(mCheckCircle, "alpha", 0f, 1f);
184 
185         mCheckCircle.setImageDrawable(mAppContext.getDrawable(R.drawable.check_circle_blue));
186 
187         AnimatorSet animatorSet = new AnimatorSet();
188         animatorSet.playTogether(paddingAnimator, checkCircleAlphaAnimator);
189         animatorSet.setDuration(200);
190         animatorSet.start();
191 
192         mCheckCircle.setVisibility(View.VISIBLE);
193 
194         mIsSelected = true;
195     }
196 
197     @Override
animateDeselected()198     public void animateDeselected() {
199         mLoadingIndicatorContainer.setVisibility(View.GONE);
200 
201         // If already deselected, do nothing.
202         if (!mIsSelected) {
203             return;
204         }
205 
206         int[][] values = new int[2][4];
207         int insetPx = mAppContext.getResources().getDimensionPixelSize(
208                 R.dimen.grid_item_individual_wallpaper_selected_inset);
209         values[0] = new int[]{insetPx, insetPx, insetPx, insetPx};
210         values[1] = new int[]{0, 0, 0, 0};
211 
212         ObjectAnimator paddingAnimator = ObjectAnimator.ofMultiInt(mTile, "padding", values);
213         ObjectAnimator checkCircleAlphaAnimator = ObjectAnimator.ofFloat(mCheckCircle, "alpha", 1f, 0f);
214 
215         AnimatorSet animatorSet = new AnimatorSet();
216         animatorSet.playTogether(paddingAnimator, checkCircleAlphaAnimator);
217         animatorSet.setDuration(200);
218         animatorSet.start();
219 
220         checkCircleAlphaAnimator.addListener(new AnimatorListener() {
221             @Override
222             public void onAnimationStart(Animator animation) {
223             }
224 
225             @Override
226             public void onAnimationEnd(Animator animation) {
227                 mCheckCircle.setVisibility(View.GONE);
228             }
229 
230             @Override
231             public void onAnimationCancel(Animator animation) {
232             }
233 
234             @Override
235             public void onAnimationRepeat(Animator animation) {
236             }
237         });
238 
239         mIsSelected = false;
240     }
241 
242     @Override
showLoading()243     public void showLoading() {
244         mLoadingIndicatorContainer.setVisibility(View.VISIBLE);
245     }
246 
247     @Override
showNotLoading()248     public void showNotLoading() {
249         mLoadingIndicatorContainer.setVisibility(View.GONE);
250     }
251 
animateHoverEnter()252     private void animateHoverEnter() {
253         removeHoverHandlerCallbacks();
254 
255         if (mIsHovered) {
256             return;
257         }
258 
259         mHoverHandler.postDelayed(mHoverEnterRunnable, HOVER_TIMEOUT_MS);
260     }
261 
animateHoverExit()262     private void animateHoverExit() {
263         removeHoverHandlerCallbacks();
264 
265         if (!mIsHovered) {
266             return;
267         }
268 
269         mHoverHandler.postDelayed(mHoverExitRunnable, HOVER_TIMEOUT_MS);
270     }
271 
272     @Override
isSelected()273     public boolean isSelected() {
274         return mIsSelected;
275     }
276 
removeHoverHandlerCallbacks()277     private void removeHoverHandlerCallbacks() {
278         mHoverHandler.removeCallbacks(mHoverEnterRunnable);
279         mHoverHandler.removeCallbacks(mHoverExitRunnable);
280     }
281 }
282