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 }