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 18package com.android.mail.ui; 19 20import android.animation.Animator.AnimatorListener; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.content.res.Resources; 24import android.util.AttributeSet; 25import android.view.View; 26import android.view.View.OnClickListener; 27import android.view.animation.DecelerateInterpolator; 28import android.widget.FrameLayout; 29import android.widget.TextView; 30 31import com.android.mail.R; 32import com.android.mail.analytics.Analytics; 33import com.android.mail.browse.ConversationCursor; 34import com.android.mail.browse.ConversationItemView; 35import com.android.mail.providers.Account; 36import com.android.mail.providers.Conversation; 37import com.android.mail.providers.Folder; 38import com.android.mail.utils.Utils; 39import com.google.common.collect.ImmutableList; 40 41public 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 } 107 } 108 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 128 public void commit() { 129 ConversationCursor cursor = mAdapter.getConversationCursor(); 130 if (cursor != null) { 131 cursor.delete(ImmutableList.of(getData())); 132 } 133 } 134 135 @Override 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 144 public long getConversationId() { 145 return getData().id; 146 } 147 148 @Override 149 public SwipeableView getSwipeableView() { 150 return SwipeableView.from(mSwipeableContent); 151 } 152 153 @Override 154 public boolean canChildBeDismissed() { 155 return !mInert; 156 } 157 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 264 public void setData(Conversation conversation) { 265 mData = conversation; 266 } 267 268 public Conversation getData() { 269 return mData; 270 } 271 272 @Override 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") 285 public void setAnimatedHeight(int height) { 286 mAnimatedHeight = height; 287 requestLayout(); 288 } 289 290 @Override 291 public float getMinAllowScrollDistance() { 292 return sScrollSlop; 293 } 294 295 public void makeInert() { 296 if (mFadeIn != null) { 297 mFadeIn.cancel(); 298 } 299 mSwipeableContent.setVisibility(View.GONE); 300 mInert = true; 301 } 302 303 public void cancelFadeOutText() { 304 mSwipeableContent.setAlpha(OPAQUE); 305 } 306 307 public boolean isAnimating() { 308 return this.mFadingInText; 309 } 310}