Home | History | Annotate | Download | only in ui
      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         }
    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 }