blob: afd44fe71b8ae438bdc0ab10f2ae2ddfa1971b11 [file] [log] [blame]
/*
* 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);
}
}
} else if (id == R.id.undo_descriptionview) {
// Essentially, makes sure that tapping description view doesn't highlight
// either the undo button icon or text.
}
}
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_descriptionview));
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 item the conversation to animate
* @param listener the method to call when the animation is done
* @param undo true if an operation is being undone. We animate the item
* away during delete. Undoing populates the item.
*/
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;
}
}