1 /*
2  * Copyright (C) 2012 Google Inc.
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mail.ui;
19 
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.ObjectAnimator;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.util.AttributeSet;
25 import android.view.View;
26 import android.view.View.OnClickListener;
27 import android.view.animation.DecelerateInterpolator;
28 import android.widget.FrameLayout;
29 import android.widget.TextView;
30 
31 import com.android.mail.R;
32 import com.android.mail.analytics.Analytics;
33 import com.android.mail.browse.ConversationCursor;
34 import com.android.mail.browse.ConversationItemView;
35 import com.android.mail.providers.Account;
36 import com.android.mail.providers.Conversation;
37 import com.android.mail.providers.Folder;
38 import com.android.mail.utils.Utils;
39 import com.google.common.collect.ImmutableList;
40 
41 public class LeaveBehindItem extends FrameLayout implements OnClickListener, SwipeableItemView {
42 
43     private ToastBarOperation mUndoOp;
44     private Account mAccount;
45     private AnimatedAdapter mAdapter;
46     private TextView mText;
47     private View mSwipeableContent;
48     public int position;
49     private Conversation mData;
50     private int mWidth;
51     /**
52      * The height of this view. Typically, this matches the height of the originating
53      * {@link ConversationItemView}.
54      */
55     private int mHeight;
56     private int mAnimatedHeight = -1;
57     private boolean mAnimating;
58     private boolean mFadingInText;
59     private boolean mInert = false;
60     private ObjectAnimator mFadeIn;
61 
62     private static int sShrinkAnimationDuration = -1;
63     private static int sFadeInAnimationDuration = -1;
64     private static float sScrollSlop;
65     private static final float OPAQUE = 1.0f;
66     private static final float TRANSPARENT = 0.0f;
67 
LeaveBehindItem(Context context)68     public LeaveBehindItem(Context context) {
69         this(context, null);
70     }
71 
LeaveBehindItem(Context context, AttributeSet attrs)72     public LeaveBehindItem(Context context, AttributeSet attrs) {
73         this(context, attrs, -1);
74     }
75 
LeaveBehindItem(Context context, AttributeSet attrs, int defStyle)76     public LeaveBehindItem(Context context, AttributeSet attrs, int defStyle) {
77         super(context, attrs, defStyle);
78         loadStatics(context);
79     }
80 
loadStatics(final Context context)81     private static void loadStatics(final Context context) {
82         if (sShrinkAnimationDuration == -1) {
83             Resources res = context.getResources();
84             sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
85             sFadeInAnimationDuration = res.getInteger(R.integer.fade_in_animation_duration);
86             sScrollSlop = res.getInteger(R.integer.leaveBehindSwipeScrollSlop);
87         }
88     }
89 
90     @Override
onClick(View v)91     public void onClick(View v) {
92         final int id = v.getId();
93         if (id == R.id.swipeable_content) {
94             if (mAccount.undoUri != null && !mInert) {
95                 // NOTE: We might want undo to return the messages affected,
96                 // in which case the resulting cursor might be interesting...
97                 // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate
98                 // the set of commands to undo
99                 mAdapter.setSwipeUndo(true);
100                 mAdapter.clearLeaveBehind(getConversationId());
101                 ConversationCursor cursor = mAdapter.getConversationCursor();
102                 if (cursor != null) {
103                     cursor.undo(getContext(), mAccount.undoUri);
104                 }
105             }
106         }
107     }
108 
bind(int pos, Account account, AnimatedAdapter adapter, ToastBarOperation undoOp, Conversation target, Folder folder, int height)109     public void bind(int pos, Account account, AnimatedAdapter adapter,
110             ToastBarOperation undoOp, Conversation target, Folder folder, int height) {
111         position = pos;
112         mUndoOp = undoOp;
113         mAccount = account;
114         mAdapter = adapter;
115         mHeight = height;
116         setData(target);
117         mSwipeableContent = findViewById(R.id.swipeable_content);
118         // Listen on swipeable content so that we can show both the undo icon
119         // and button text as selected since they set duplicateParentState to true
120         mSwipeableContent.setOnClickListener(this);
121         mSwipeableContent.setAlpha(TRANSPARENT);
122         mText = ((TextView) findViewById(R.id.undo_description_text));
123         mText.setText(Utils.convertHtmlToPlainText(mUndoOp
124                 .getSingularDescription(getContext(), folder)));
125         mText.setOnClickListener(this);
126     }
127 
commit()128     public void commit() {
129         ConversationCursor cursor = mAdapter.getConversationCursor();
130         if (cursor != null) {
131             cursor.delete(ImmutableList.of(getData()));
132         }
133     }
134 
135     @Override
dismiss()136     public void dismiss() {
137         if (mAdapter != null) {
138             Analytics.getInstance().sendEvent("list_swipe", "leave_behind", null, 0);
139             mAdapter.fadeOutSpecificLeaveBehindItem(mData.id);
140             mAdapter.notifyDataSetChanged();
141         }
142     }
143 
getConversationId()144     public long getConversationId() {
145         return getData().id;
146     }
147 
148     @Override
getSwipeableView()149     public SwipeableView getSwipeableView() {
150         return SwipeableView.from(mSwipeableContent);
151     }
152 
153     @Override
canChildBeDismissed()154     public boolean canChildBeDismissed() {
155         return !mInert;
156     }
157 
getLeaveBehindData()158     public LeaveBehindData getLeaveBehindData() {
159         return new LeaveBehindData(getData(), mUndoOp, mHeight);
160     }
161 
162     /**
163      * Animate shrinking the height of this view.
164      * @param listener the method to call when the animation is done
165      */
startShrinkAnimation(AnimatorListener listener)166     public void startShrinkAnimation(AnimatorListener listener) {
167         if (!mAnimating) {
168             mAnimating = true;
169             final ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", mHeight, 0);
170             setMinimumHeight(mHeight);
171             mWidth = getWidth();
172             height.setInterpolator(new DecelerateInterpolator(1.75f));
173             height.setDuration(sShrinkAnimationDuration);
174             height.addListener(listener);
175             height.start();
176         }
177     }
178 
179     /**
180      * Set the alpha value for the text displayed by this item.
181      */
setTextAlpha(float alpha)182     public void setTextAlpha(float alpha) {
183         if (mSwipeableContent.getAlpha() > TRANSPARENT) {
184             mSwipeableContent.setAlpha(alpha);
185         }
186     }
187 
188     /**
189      * Kick off the animation to fade in the leave behind text.
190      * @param delay Whether to delay the start of the animation or not.
191      */
startFadeInTextAnimation(int delay)192     public void startFadeInTextAnimation(int delay) {
193         // If this thing isn't already fully visible AND its not already animating...
194         if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) {
195             mFadingInText = true;
196             mFadeIn = startFadeInTextAnimation(mSwipeableContent, delay);
197         }
198     }
199 
200     /**
201      * Creates and starts the animator for the fade-in text
202      * @param delay The delay, in milliseconds, before starting the animation
203      * @return The {@link ObjectAnimator}
204      */
startFadeInTextAnimation(final View view, final int delay)205     public static ObjectAnimator startFadeInTextAnimation(final View view, final int delay) {
206         loadStatics(view.getContext());
207 
208         final float start = TRANSPARENT;
209         final float end = OPAQUE;
210         final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", start, end);
211         view.setAlpha(TRANSPARENT);
212         if (delay != 0) {
213             fadeIn.setStartDelay(delay);
214         }
215         fadeIn.setInterpolator(new DecelerateInterpolator(OPAQUE));
216         fadeIn.setDuration(sFadeInAnimationDuration / 2);
217         fadeIn.start();
218 
219         return fadeIn;
220     }
221 
222     /**
223      * Increase the overall time before fading in a the text description this view.
224      * @param newDelay Amount of total delay the user should see
225      */
increaseFadeInDelay(int newDelay)226     public void increaseFadeInDelay(int newDelay) {
227         // If this thing isn't already fully visible AND its not already animating...
228         if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) {
229             mFadingInText = true;
230             long delay = mFadeIn.getStartDelay();
231             if (newDelay == delay || mFadeIn.isRunning()) {
232                 return;
233             }
234             mFadeIn.cancel();
235             mFadeIn.setStartDelay(newDelay - delay);
236             mFadeIn.start();
237         }
238     }
239 
240     /**
241      * Cancel fading in the text description for this view.
242      */
cancelFadeInTextAnimation()243     public void cancelFadeInTextAnimation() {
244         if (mFadeIn != null) {
245             mFadingInText = false;
246             mFadeIn.cancel();
247         }
248     }
249 
250     /**
251      * Cancel fading in the text description for this view only if it the
252      * animation hasn't already started.
253      * @return whether the animation was cancelled
254      */
cancelFadeInTextAnimationIfNotStarted()255     public boolean cancelFadeInTextAnimationIfNotStarted() {
256         // The animation was started, so don't cancel and restart it.
257         if (mFadeIn != null && !mFadeIn.isRunning()) {
258             cancelFadeInTextAnimation();
259             return true;
260         }
261         return false;
262     }
263 
setData(Conversation conversation)264     public void setData(Conversation conversation) {
265         mData = conversation;
266     }
267 
getData()268     public Conversation getData() {
269         return mData;
270     }
271 
272     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)273     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
274         if (mAnimatedHeight != -1) {
275             setMeasuredDimension(mWidth, mAnimatedHeight);
276         } else {
277             // override the height MeasureSpec to ensure this is sized up at the desired height
278             super.onMeasure(widthMeasureSpec,
279                     MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
280         }
281     }
282 
283     // Used by animator
284     @SuppressWarnings("unused")
setAnimatedHeight(int height)285     public void setAnimatedHeight(int height) {
286         mAnimatedHeight = height;
287         requestLayout();
288     }
289 
290     @Override
getMinAllowScrollDistance()291     public float getMinAllowScrollDistance() {
292         return sScrollSlop;
293     }
294 
makeInert()295     public void makeInert() {
296         if (mFadeIn != null) {
297             mFadeIn.cancel();
298         }
299         mSwipeableContent.setVisibility(View.GONE);
300         mInert = true;
301     }
302 
cancelFadeOutText()303     public void cancelFadeOutText() {
304         mSwipeableContent.setAlpha(OPAQUE);
305     }
306 
isAnimating()307     public boolean isAnimating() {
308         return this.mFadingInText;
309     }
310 }