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         } 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 }