Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.messaging.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.ObjectAnimator;
     21 import android.content.Context;
     22 import android.graphics.Rect;
     23 import android.os.Handler;
     24 import android.os.Looper;
     25 import android.util.AttributeSet;
     26 import android.view.LayoutInflater;
     27 import android.view.View;
     28 import android.widget.FrameLayout;
     29 import android.widget.ImageButton;
     30 import android.widget.ScrollView;
     31 
     32 import com.android.messaging.R;
     33 import com.android.messaging.annotation.VisibleForAnimation;
     34 import com.android.messaging.datamodel.data.DraftMessageData;
     35 import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
     36 import com.android.messaging.datamodel.data.MessagePartData;
     37 import com.android.messaging.datamodel.data.PendingAttachmentData;
     38 import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener;
     39 import com.android.messaging.ui.animation.PopupTransitionAnimation;
     40 import com.android.messaging.ui.conversation.ComposeMessageView;
     41 import com.android.messaging.ui.conversation.ConversationFragment;
     42 import com.android.messaging.util.Assert;
     43 import com.android.messaging.util.ThreadUtil;
     44 import com.android.messaging.util.UiUtils;
     45 
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 
     49 public class AttachmentPreview extends ScrollView implements OnAttachmentClickListener {
     50     private FrameLayout mAttachmentView;
     51     private ComposeMessageView mComposeMessageView;
     52     private ImageButton mCloseButton;
     53     private int mAnimatedHeight = -1;
     54     private Animator mCloseGapAnimator;
     55     private boolean mPendingFirstUpdate;
     56     private Handler mHandler;
     57     private Runnable mHideRunnable;
     58     private boolean mPendingHideCanceled;
     59 
     60     private static final int CLOSE_BUTTON_REVEAL_STAGGER_MILLIS = 300;
     61 
     62     public AttachmentPreview(final Context context, final AttributeSet attrs) {
     63         super(context, attrs);
     64         mHandler = new Handler(Looper.getMainLooper());
     65     }
     66 
     67     @Override
     68     protected void onFinishInflate() {
     69         super.onFinishInflate();
     70         mCloseButton = (ImageButton) findViewById(R.id.close_button);
     71         mCloseButton.setOnClickListener(new OnClickListener() {
     72             @Override
     73             public void onClick(final View view) {
     74                 mComposeMessageView.clearAttachments();
     75             }
     76         });
     77 
     78         mAttachmentView = (FrameLayout) findViewById(R.id.attachment_view);
     79 
     80         // The attachment preview is a scroll view so that it can show the bottom portion of the
     81         // attachment whenever the space is tight (e.g. when in landscape mode). Per design
     82         // request we'd like to make the attachment view always scrolled to the bottom.
     83         addOnLayoutChangeListener(new OnLayoutChangeListener() {
     84             @Override
     85             public void onLayoutChange(final View v, final int left, final int top, final int right,
     86                     final int bottom, final int oldLeft, final int oldTop, final int oldRight,
     87                     final int oldBottom) {
     88                 post(new Runnable() {
     89                     @Override
     90                     public void run() {
     91                         final int childCount = getChildCount();
     92                         if (childCount > 0) {
     93                             final View lastChild = getChildAt(childCount - 1);
     94                             scrollTo(getScrollX(), lastChild.getBottom() - getHeight());
     95                         }
     96                     }
     97                 });
     98             }
     99         });
    100         mPendingFirstUpdate = true;
    101     }
    102 
    103     public void setComposeMessageView(final ComposeMessageView composeMessageView) {
    104         mComposeMessageView = composeMessageView;
    105     }
    106 
    107     @Override
    108     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    109         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    110         if (mAnimatedHeight >= 0) {
    111             setMeasuredDimension(getMeasuredWidth(), mAnimatedHeight);
    112         }
    113     }
    114 
    115     private void cancelPendingHide() {
    116         mPendingHideCanceled = true;
    117     }
    118 
    119     public void hideAttachmentPreview() {
    120         if (getVisibility() != GONE) {
    121             UiUtils.revealOrHideViewWithAnimation(mCloseButton, GONE,
    122                     null /* onFinishRunnable */);
    123             startCloseGapAnimationOnAttachmentClear();
    124 
    125             if (mAttachmentView.getChildCount() > 0) {
    126                 mPendingHideCanceled = false;
    127                 final View viewToHide = mAttachmentView.getChildCount() > 1 ?
    128                         mAttachmentView : mAttachmentView.getChildAt(0);
    129                 UiUtils.revealOrHideViewWithAnimation(viewToHide, INVISIBLE,
    130                         new Runnable() {
    131                             @Override
    132                             public void run() {
    133                                 // Only hide if we are didn't get overruled by showing
    134                                 if (!mPendingHideCanceled) {
    135                                     mAttachmentView.removeAllViews();
    136                                     setVisibility(GONE);
    137                                 }
    138                             }
    139                         });
    140             } else {
    141                 mAttachmentView.removeAllViews();
    142                 setVisibility(GONE);
    143             }
    144         }
    145     }
    146 
    147     // returns true if we have attachments
    148     public boolean onAttachmentsChanged(final DraftMessageData draftMessageData) {
    149         final boolean isFirstUpdate = mPendingFirstUpdate;
    150         final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments();
    151         final List<PendingAttachmentData> pendingAttachments =
    152                 draftMessageData.getReadOnlyPendingAttachments();
    153 
    154         // Any change in attachments would invalidate the animated height animation.
    155         cancelCloseGapAnimation();
    156         mPendingFirstUpdate = false;
    157 
    158         final int combinedAttachmentCount = attachments.size() + pendingAttachments.size();
    159         mCloseButton.setContentDescription(getResources()
    160                 .getQuantityString(R.plurals.attachment_preview_close_content_description,
    161                         combinedAttachmentCount));
    162         if (combinedAttachmentCount == 0) {
    163             mHideRunnable = new Runnable() {
    164                 @Override
    165                 public void run() {
    166                     mHideRunnable = null;
    167                     // Only start the hiding if there are still no attachments
    168                     if (attachments.size() + pendingAttachments.size() == 0) {
    169                         hideAttachmentPreview();
    170                     }
    171                 }
    172             };
    173             if (draftMessageData.isSending()) {
    174                 // Wait to hide until the message is ready to start animating
    175                 // We'll execute immediately when the animation triggers
    176                 mHandler.postDelayed(mHideRunnable,
    177                         ConversationFragment.MESSAGE_ANIMATION_MAX_WAIT);
    178             } else {
    179                 // Run immediately when clearing attachments
    180                 mHideRunnable.run();
    181             }
    182             return false;
    183         }
    184 
    185         cancelPendingHide();  // We're showing
    186         if (getVisibility() != VISIBLE) {
    187             setVisibility(VISIBLE);
    188             mAttachmentView.setVisibility(VISIBLE);
    189 
    190             // Don't animate in the close button if this is the first update after view creation.
    191             // This is the initial draft load from database for pre-existing drafts.
    192             if (!isFirstUpdate) {
    193                 // Reveal the close button after the view animates in.
    194                 mCloseButton.setVisibility(INVISIBLE);
    195                 ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() {
    196                     @Override
    197                     public void run() {
    198                         UiUtils.revealOrHideViewWithAnimation(mCloseButton, VISIBLE,
    199                                 null /* onFinishRunnable */);
    200                     }
    201                 }, UiUtils.MEDIAPICKER_TRANSITION_DURATION + CLOSE_BUTTON_REVEAL_STAGGER_MILLIS);
    202             }
    203         }
    204 
    205         // Merge the pending attachment list with real attachment.  Design would prefer these be
    206         // in LIFO order user can see added images past the 5th one but we also want them to be in
    207         // order and we want it to be WYSIWYG.
    208         final List<MessagePartData> combinedAttachments = new ArrayList<>();
    209         combinedAttachments.addAll(attachments);
    210         combinedAttachments.addAll(pendingAttachments);
    211 
    212         final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
    213         if (combinedAttachmentCount > 1) {
    214             MultiAttachmentLayout multiAttachmentLayout = null;
    215             Rect transitionRect = null;
    216             if (mAttachmentView.getChildCount() > 0) {
    217                 final View firstChild = mAttachmentView.getChildAt(0);
    218                 if (firstChild instanceof MultiAttachmentLayout) {
    219                     Assert.equals(1, mAttachmentView.getChildCount());
    220                     multiAttachmentLayout = (MultiAttachmentLayout) firstChild;
    221                     multiAttachmentLayout.bindAttachments(combinedAttachments,
    222                             null /* transitionRect */, combinedAttachmentCount);
    223                 } else {
    224                     transitionRect = new Rect(firstChild.getLeft(), firstChild.getTop(),
    225                             firstChild.getRight(), firstChild.getBottom());
    226                 }
    227             }
    228             if (multiAttachmentLayout == null) {
    229                 multiAttachmentLayout = AttachmentPreviewFactory.createMultiplePreview(
    230                         getContext(), this);
    231                 multiAttachmentLayout.bindAttachments(combinedAttachments, transitionRect,
    232                         combinedAttachmentCount);
    233                 mAttachmentView.removeAllViews();
    234                 mAttachmentView.addView(multiAttachmentLayout);
    235             }
    236         } else {
    237             final MessagePartData attachment = combinedAttachments.get(0);
    238             boolean shouldAnimate = true;
    239             if (mAttachmentView.getChildCount() > 0) {
    240                 // If we are going from N->1 attachments, try to use the current bounds
    241                 // bounds as the starting rect.
    242                 shouldAnimate = false;
    243                 final View firstChild = mAttachmentView.getChildAt(0);
    244                 if (firstChild instanceof MultiAttachmentLayout &&
    245                         attachment instanceof MediaPickerMessagePartData) {
    246                     final View leftoverView = ((MultiAttachmentLayout) firstChild)
    247                             .findViewForAttachment(attachment);
    248                     if (leftoverView != null) {
    249                         final Rect currentRect = UiUtils.getMeasuredBoundsOnScreen(leftoverView);
    250                         if (!currentRect.isEmpty() &&
    251                                 attachment instanceof MediaPickerMessagePartData) {
    252                             ((MediaPickerMessagePartData) attachment).setStartRect(currentRect);
    253                             shouldAnimate = true;
    254                         }
    255                     }
    256                 }
    257             }
    258             mAttachmentView.removeAllViews();
    259             final View attachmentView = AttachmentPreviewFactory.createAttachmentPreview(
    260                     layoutInflater, attachment, mAttachmentView,
    261                     AttachmentPreviewFactory.TYPE_SINGLE, true /* startImageRequest */, this);
    262             if (attachmentView != null) {
    263                 mAttachmentView.addView(attachmentView);
    264                 if (shouldAnimate) {
    265                     tryAnimateViewIn(attachment, attachmentView);
    266                 }
    267             }
    268         }
    269         return true;
    270     }
    271 
    272     public void onMessageAnimationStart() {
    273         if (mHideRunnable == null) {
    274             return;
    275         }
    276 
    277         // Run the hide animation at the same time as the message animation
    278         mHandler.removeCallbacks(mHideRunnable);
    279         setVisibility(View.INVISIBLE);
    280         mHideRunnable.run();
    281     }
    282 
    283     static void tryAnimateViewIn(final MessagePartData attachmentData, final View view) {
    284         if (attachmentData instanceof MediaPickerMessagePartData) {
    285             final Rect startRect = ((MediaPickerMessagePartData) attachmentData).getStartRect();
    286             new PopupTransitionAnimation(startRect, view).startAfterLayoutComplete();
    287         }
    288     }
    289 
    290     @VisibleForAnimation
    291     public void setAnimatedHeight(final int animatedHeight) {
    292         if (mAnimatedHeight != animatedHeight) {
    293             mAnimatedHeight = animatedHeight;
    294             requestLayout();
    295         }
    296     }
    297 
    298     /**
    299      * Kicks off an animation to animate the layout change for closing the gap between the
    300      * message list and the compose message box when the attachments are cleared.
    301      */
    302     private void startCloseGapAnimationOnAttachmentClear() {
    303         // Cancel existing animation.
    304         cancelCloseGapAnimation();
    305         mCloseGapAnimator = ObjectAnimator.ofInt(this, "animatedHeight", getHeight(), 0);
    306         mCloseGapAnimator.start();
    307     }
    308 
    309     private void cancelCloseGapAnimation() {
    310         if (mCloseGapAnimator != null) {
    311             mCloseGapAnimator.cancel();
    312             mCloseGapAnimator = null;
    313         }
    314         mAnimatedHeight = -1;
    315     }
    316 
    317     @Override
    318     public boolean onAttachmentClick(final MessagePartData attachment,
    319             final Rect viewBoundsOnScreen, final boolean longPress) {
    320         if (longPress) {
    321             mComposeMessageView.onAttachmentPreviewLongClicked();
    322             return true;
    323         }
    324 
    325         if (!(attachment instanceof PendingAttachmentData) && attachment.isImage()) {
    326             mComposeMessageView.displayPhoto(attachment.getContentUri(), viewBoundsOnScreen);
    327             return true;
    328         }
    329         return false;
    330     }
    331 }
    332