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 68 public LeaveBehindItem(Context context) { 69 this(context, null); 70 } 71 72 public LeaveBehindItem(Context context, AttributeSet attrs) { 73 this(context, attrs, -1); 74 } 75 76 public LeaveBehindItem(Context context, AttributeSet attrs, int defStyle) { 77 super(context, attrs, defStyle); 78 loadStatics(context); 79 } 80 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 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 } else if (id == R.id.undo_descriptionview) { 107 // Essentially, makes sure that tapping description view doesn't highlight 108 // either the undo button icon or text. 109 } 110 } 111 112 public void bind(int pos, Account account, AnimatedAdapter adapter, 113 ToastBarOperation undoOp, Conversation target, Folder folder, int height) { 114 position = pos; 115 mUndoOp = undoOp; 116 mAccount = account; 117 mAdapter = adapter; 118 mHeight = height; 119 setData(target); 120 mSwipeableContent = findViewById(R.id.swipeable_content); 121 // Listen on swipeable content so that we can show both the undo icon 122 // and button text as selected since they set duplicateParentState to true 123 mSwipeableContent.setOnClickListener(this); 124 mSwipeableContent.setAlpha(TRANSPARENT); 125 mText = ((TextView) findViewById(R.id.undo_descriptionview)); 126 mText.setText(Utils.convertHtmlToPlainText(mUndoOp 127 .getSingularDescription(getContext(), folder))); 128 mText.setOnClickListener(this); 129 } 130 131 public void commit() { 132 ConversationCursor cursor = mAdapter.getConversationCursor(); 133 if (cursor != null) { 134 cursor.delete(ImmutableList.of(getData())); 135 } 136 } 137 138 @Override 139 public void dismiss() { 140 if (mAdapter != null) { 141 Analytics.getInstance().sendEvent("list_swipe", "leave_behind", null, 0); 142 mAdapter.fadeOutSpecificLeaveBehindItem(mData.id); 143 mAdapter.notifyDataSetChanged(); 144 } 145 } 146 147 public long getConversationId() { 148 return getData().id; 149 } 150 151 @Override 152 public SwipeableView getSwipeableView() { 153 return SwipeableView.from(mSwipeableContent); 154 } 155 156 @Override 157 public boolean canChildBeDismissed() { 158 return !mInert; 159 } 160 161 public LeaveBehindData getLeaveBehindData() { 162 return new LeaveBehindData(getData(), mUndoOp, mHeight); 163 } 164 165 /** 166 * Animate shrinking the height of this view. 167 * @param item the conversation to animate 168 * @param listener the method to call when the animation is done 169 * @param undo true if an operation is being undone. We animate the item 170 * away during delete. Undoing populates the item. 171 */ 172 public void startShrinkAnimation(AnimatorListener listener) { 173 if (!mAnimating) { 174 mAnimating = true; 175 final ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", mHeight, 0); 176 setMinimumHeight(mHeight); 177 mWidth = getWidth(); 178 height.setInterpolator(new DecelerateInterpolator(1.75f)); 179 height.setDuration(sShrinkAnimationDuration); 180 height.addListener(listener); 181 height.start(); 182 } 183 } 184 185 /** 186 * Set the alpha value for the text displayed by this item. 187 */ 188 public void setTextAlpha(float alpha) { 189 if (mSwipeableContent.getAlpha() > TRANSPARENT) { 190 mSwipeableContent.setAlpha(alpha); 191 } 192 } 193 194 /** 195 * Kick off the animation to fade in the leave behind text. 196 * @param delay Whether to delay the start of the animation or not. 197 */ 198 public void startFadeInTextAnimation(int delay) { 199 // If this thing isn't already fully visible AND its not already animating... 200 if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) { 201 mFadingInText = true; 202 mFadeIn = startFadeInTextAnimation(mSwipeableContent, delay); 203 } 204 } 205 206 /** 207 * Creates and starts the animator for the fade-in text 208 * @param delay The delay, in milliseconds, before starting the animation 209 * @return The {@link ObjectAnimator} 210 */ 211 public static ObjectAnimator startFadeInTextAnimation(final View view, final int delay) { 212 loadStatics(view.getContext()); 213 214 final float start = TRANSPARENT; 215 final float end = OPAQUE; 216 final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", start, end); 217 view.setAlpha(TRANSPARENT); 218 if (delay != 0) { 219 fadeIn.setStartDelay(delay); 220 } 221 fadeIn.setInterpolator(new DecelerateInterpolator(OPAQUE)); 222 fadeIn.setDuration(sFadeInAnimationDuration / 2); 223 fadeIn.start(); 224 225 return fadeIn; 226 } 227 228 /** 229 * Increase the overall time before fading in a the text description this view. 230 * @param newDelay Amount of total delay the user should see 231 */ 232 public void increaseFadeInDelay(int newDelay) { 233 // If this thing isn't already fully visible AND its not already animating... 234 if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) { 235 mFadingInText = true; 236 long delay = mFadeIn.getStartDelay(); 237 if (newDelay == delay || mFadeIn.isRunning()) { 238 return; 239 } 240 mFadeIn.cancel(); 241 mFadeIn.setStartDelay(newDelay - delay); 242 mFadeIn.start(); 243 } 244 } 245 246 /** 247 * Cancel fading in the text description for this view. 248 */ 249 public void cancelFadeInTextAnimation() { 250 if (mFadeIn != null) { 251 mFadingInText = false; 252 mFadeIn.cancel(); 253 } 254 } 255 256 /** 257 * Cancel fading in the text description for this view only if it the 258 * animation hasn't already started. 259 * @return whether the animation was cancelled 260 */ 261 public boolean cancelFadeInTextAnimationIfNotStarted() { 262 // The animation was started, so don't cancel and restart it. 263 if (mFadeIn != null && !mFadeIn.isRunning()) { 264 cancelFadeInTextAnimation(); 265 return true; 266 } 267 return false; 268 } 269 270 public void setData(Conversation conversation) { 271 mData = conversation; 272 } 273 274 public Conversation getData() { 275 return mData; 276 } 277 278 @Override 279 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 280 if (mAnimatedHeight != -1) { 281 setMeasuredDimension(mWidth, mAnimatedHeight); 282 } else { 283 // override the height MeasureSpec to ensure this is sized up at the desired height 284 super.onMeasure(widthMeasureSpec, 285 MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY)); 286 } 287 } 288 289 // Used by animator 290 @SuppressWarnings("unused") 291 public void setAnimatedHeight(int height) { 292 mAnimatedHeight = height; 293 requestLayout(); 294 } 295 296 @Override 297 public float getMinAllowScrollDistance() { 298 return sScrollSlop; 299 } 300 301 public void makeInert() { 302 if (mFadeIn != null) { 303 mFadeIn.cancel(); 304 } 305 mSwipeableContent.setVisibility(View.GONE); 306 mInert = true; 307 } 308 309 public void cancelFadeOutText() { 310 mSwipeableContent.setAlpha(OPAQUE); 311 } 312 313 public boolean isAnimating() { 314 return this.mFadingInText; 315 } 316 }