/* * Copyright (C) 2012 Google Inc. * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.ui; import android.animation.Animator.AnimatorListener; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.TextView; import com.android.mail.R; import com.android.mail.analytics.Analytics; import com.android.mail.browse.ConversationCursor; import com.android.mail.browse.ConversationItemView; import com.android.mail.providers.Account; import com.android.mail.providers.Conversation; import com.android.mail.providers.Folder; import com.android.mail.utils.Utils; import com.google.common.collect.ImmutableList; public class LeaveBehindItem extends FrameLayout implements OnClickListener, SwipeableItemView { private ToastBarOperation mUndoOp; private Account mAccount; private AnimatedAdapter mAdapter; private TextView mText; private View mSwipeableContent; public int position; private Conversation mData; private int mWidth; /** * The height of this view. Typically, this matches the height of the originating * {@link ConversationItemView}. */ private int mHeight; private int mAnimatedHeight = -1; private boolean mAnimating; private boolean mFadingInText; private boolean mInert = false; private ObjectAnimator mFadeIn; private static int sShrinkAnimationDuration = -1; private static int sFadeInAnimationDuration = -1; private static float sScrollSlop; private static final float OPAQUE = 1.0f; private static final float TRANSPARENT = 0.0f; public LeaveBehindItem(Context context) { this(context, null); } public LeaveBehindItem(Context context, AttributeSet attrs) { this(context, attrs, -1); } public LeaveBehindItem(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); loadStatics(context); } private static void loadStatics(final Context context) { if (sShrinkAnimationDuration == -1) { Resources res = context.getResources(); sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration); sFadeInAnimationDuration = res.getInteger(R.integer.fade_in_animation_duration); sScrollSlop = res.getInteger(R.integer.leaveBehindSwipeScrollSlop); } } @Override public void onClick(View v) { final int id = v.getId(); if (id == R.id.swipeable_content) { if (mAccount.undoUri != null && !mInert) { // NOTE: We might want undo to return the messages affected, // in which case the resulting cursor might be interesting... // TODO: Use UIProvider.SEQUENCE_QUERY_PARAMETER to indicate // the set of commands to undo mAdapter.setSwipeUndo(true); mAdapter.clearLeaveBehind(getConversationId()); ConversationCursor cursor = mAdapter.getConversationCursor(); if (cursor != null) { cursor.undo(getContext(), mAccount.undoUri); } } } } public void bind(int pos, Account account, AnimatedAdapter adapter, ToastBarOperation undoOp, Conversation target, Folder folder, int height) { position = pos; mUndoOp = undoOp; mAccount = account; mAdapter = adapter; mHeight = height; setData(target); mSwipeableContent = findViewById(R.id.swipeable_content); // Listen on swipeable content so that we can show both the undo icon // and button text as selected since they set duplicateParentState to true mSwipeableContent.setOnClickListener(this); mSwipeableContent.setAlpha(TRANSPARENT); mText = ((TextView) findViewById(R.id.undo_description_text)); mText.setText(Utils.convertHtmlToPlainText(mUndoOp .getSingularDescription(getContext(), folder))); mText.setOnClickListener(this); } public void commit() { ConversationCursor cursor = mAdapter.getConversationCursor(); if (cursor != null) { cursor.delete(ImmutableList.of(getData())); } } @Override public void dismiss() { if (mAdapter != null) { Analytics.getInstance().sendEvent("list_swipe", "leave_behind", null, 0); mAdapter.fadeOutSpecificLeaveBehindItem(mData.id); mAdapter.notifyDataSetChanged(); } } public long getConversationId() { return getData().id; } @Override public SwipeableView getSwipeableView() { return SwipeableView.from(mSwipeableContent); } @Override public boolean canChildBeDismissed() { return !mInert; } public LeaveBehindData getLeaveBehindData() { return new LeaveBehindData(getData(), mUndoOp, mHeight); } /** * Animate shrinking the height of this view. * @param listener the method to call when the animation is done */ public void startShrinkAnimation(AnimatorListener listener) { if (!mAnimating) { mAnimating = true; final ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", mHeight, 0); setMinimumHeight(mHeight); mWidth = getWidth(); height.setInterpolator(new DecelerateInterpolator(1.75f)); height.setDuration(sShrinkAnimationDuration); height.addListener(listener); height.start(); } } /** * Set the alpha value for the text displayed by this item. */ public void setTextAlpha(float alpha) { if (mSwipeableContent.getAlpha() > TRANSPARENT) { mSwipeableContent.setAlpha(alpha); } } /** * Kick off the animation to fade in the leave behind text. * @param delay Whether to delay the start of the animation or not. */ public void startFadeInTextAnimation(int delay) { // If this thing isn't already fully visible AND its not already animating... if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) { mFadingInText = true; mFadeIn = startFadeInTextAnimation(mSwipeableContent, delay); } } /** * Creates and starts the animator for the fade-in text * @param delay The delay, in milliseconds, before starting the animation * @return The {@link ObjectAnimator} */ public static ObjectAnimator startFadeInTextAnimation(final View view, final int delay) { loadStatics(view.getContext()); final float start = TRANSPARENT; final float end = OPAQUE; final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", start, end); view.setAlpha(TRANSPARENT); if (delay != 0) { fadeIn.setStartDelay(delay); } fadeIn.setInterpolator(new DecelerateInterpolator(OPAQUE)); fadeIn.setDuration(sFadeInAnimationDuration / 2); fadeIn.start(); return fadeIn; } /** * Increase the overall time before fading in a the text description this view. * @param newDelay Amount of total delay the user should see */ public void increaseFadeInDelay(int newDelay) { // If this thing isn't already fully visible AND its not already animating... if (!mFadingInText && mSwipeableContent.getAlpha() != OPAQUE) { mFadingInText = true; long delay = mFadeIn.getStartDelay(); if (newDelay == delay || mFadeIn.isRunning()) { return; } mFadeIn.cancel(); mFadeIn.setStartDelay(newDelay - delay); mFadeIn.start(); } } /** * Cancel fading in the text description for this view. */ public void cancelFadeInTextAnimation() { if (mFadeIn != null) { mFadingInText = false; mFadeIn.cancel(); } } /** * Cancel fading in the text description for this view only if it the * animation hasn't already started. * @return whether the animation was cancelled */ public boolean cancelFadeInTextAnimationIfNotStarted() { // The animation was started, so don't cancel and restart it. if (mFadeIn != null && !mFadeIn.isRunning()) { cancelFadeInTextAnimation(); return true; } return false; } public void setData(Conversation conversation) { mData = conversation; } public Conversation getData() { return mData; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mAnimatedHeight != -1) { setMeasuredDimension(mWidth, mAnimatedHeight); } else { // override the height MeasureSpec to ensure this is sized up at the desired height super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY)); } } // Used by animator @SuppressWarnings("unused") public void setAnimatedHeight(int height) { mAnimatedHeight = height; requestLayout(); } @Override public float getMinAllowScrollDistance() { return sScrollSlop; } public void makeInert() { if (mFadeIn != null) { mFadeIn.cancel(); } mSwipeableContent.setVisibility(View.GONE); mInert = true; } public void cancelFadeOutText() { mSwipeableContent.setAlpha(OPAQUE); } public boolean isAnimating() { return this.mFadingInText; } }