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 PopupTransitionAnimation mPopupTransitionAnimation;
61 
62     private static final int CLOSE_BUTTON_REVEAL_STAGGER_MILLIS = 300;
63 
AttachmentPreview(final Context context, final AttributeSet attrs)64     public AttachmentPreview(final Context context, final AttributeSet attrs) {
65         super(context, attrs);
66         mHandler = new Handler(Looper.getMainLooper());
67     }
68 
69     @Override
onFinishInflate()70     protected void onFinishInflate() {
71         super.onFinishInflate();
72         mCloseButton = (ImageButton) findViewById(R.id.close_button);
73         mCloseButton.setOnClickListener(new OnClickListener() {
74             @Override
75             public void onClick(final View view) {
76                 mComposeMessageView.clearAttachments();
77             }
78         });
79 
80         mAttachmentView = (FrameLayout) findViewById(R.id.attachment_view);
81 
82         // The attachment preview is a scroll view so that it can show the bottom portion of the
83         // attachment whenever the space is tight (e.g. when in landscape mode). Per design
84         // request we'd like to make the attachment view always scrolled to the bottom.
85         addOnLayoutChangeListener(new OnLayoutChangeListener() {
86             @Override
87             public void onLayoutChange(final View v, final int left, final int top, final int right,
88                     final int bottom, final int oldLeft, final int oldTop, final int oldRight,
89                     final int oldBottom) {
90                 post(new Runnable() {
91                     @Override
92                     public void run() {
93                         final int childCount = getChildCount();
94                         if (childCount > 0) {
95                             final View lastChild = getChildAt(childCount - 1);
96                             scrollTo(getScrollX(), lastChild.getBottom() - getHeight());
97                         }
98                     }
99                 });
100             }
101         });
102         mPendingFirstUpdate = true;
103     }
104 
setComposeMessageView(final ComposeMessageView composeMessageView)105     public void setComposeMessageView(final ComposeMessageView composeMessageView) {
106         mComposeMessageView = composeMessageView;
107     }
108 
109     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)110     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
111         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
112         if (mAnimatedHeight >= 0) {
113             setMeasuredDimension(getMeasuredWidth(), mAnimatedHeight);
114         }
115     }
116 
cancelPendingHide()117     private void cancelPendingHide() {
118         mPendingHideCanceled = true;
119     }
120 
hideAttachmentPreview()121     public void hideAttachmentPreview() {
122         if (getVisibility() != GONE) {
123             UiUtils.revealOrHideViewWithAnimation(mCloseButton, GONE,
124                     null /* onFinishRunnable */);
125             startCloseGapAnimationOnAttachmentClear();
126 
127             if (mAttachmentView.getChildCount() > 0) {
128                 mPendingHideCanceled = false;
129                 final View viewToHide = mAttachmentView.getChildCount() > 1 ?
130                         mAttachmentView : mAttachmentView.getChildAt(0);
131                 UiUtils.revealOrHideViewWithAnimation(viewToHide, INVISIBLE,
132                         new Runnable() {
133                             @Override
134                             public void run() {
135                                 // Only hide if we are didn't get overruled by showing
136                                 if (!mPendingHideCanceled) {
137                                     stopPopupAnimation();
138                                     mAttachmentView.removeAllViews();
139                                     setVisibility(GONE);
140                                 }
141                             }
142                         });
143             } else {
144                 mAttachmentView.removeAllViews();
145                 setVisibility(GONE);
146             }
147         }
148     }
149 
150     // returns true if we have attachments
onAttachmentsChanged(final DraftMessageData draftMessageData)151     public boolean onAttachmentsChanged(final DraftMessageData draftMessageData) {
152         final boolean isFirstUpdate = mPendingFirstUpdate;
153         final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments();
154         final List<PendingAttachmentData> pendingAttachments =
155                 draftMessageData.getReadOnlyPendingAttachments();
156 
157         // Any change in attachments would invalidate the animated height animation.
158         cancelCloseGapAnimation();
159         mPendingFirstUpdate = false;
160 
161         final int combinedAttachmentCount = attachments.size() + pendingAttachments.size();
162         mCloseButton.setContentDescription(getResources()
163                 .getQuantityString(R.plurals.attachment_preview_close_content_description,
164                         combinedAttachmentCount));
165         if (combinedAttachmentCount == 0) {
166             mHideRunnable = new Runnable() {
167                 @Override
168                 public void run() {
169                     mHideRunnable = null;
170                     // Only start the hiding if there are still no attachments
171                     if (attachments.size() + pendingAttachments.size() == 0) {
172                         hideAttachmentPreview();
173                     }
174                 }
175             };
176             if (draftMessageData.isSending()) {
177                 // Wait to hide until the message is ready to start animating
178                 // We'll execute immediately when the animation triggers
179                 mHandler.postDelayed(mHideRunnable,
180                         ConversationFragment.MESSAGE_ANIMATION_MAX_WAIT);
181             } else {
182                 // Run immediately when clearing attachments
183                 mHideRunnable.run();
184             }
185             return false;
186         }
187 
188         cancelPendingHide();  // We're showing
189         if (getVisibility() != VISIBLE) {
190             setVisibility(VISIBLE);
191             mAttachmentView.setVisibility(VISIBLE);
192 
193             // Don't animate in the close button if this is the first update after view creation.
194             // This is the initial draft load from database for pre-existing drafts.
195             if (!isFirstUpdate) {
196                 // Reveal the close button after the view animates in.
197                 mCloseButton.setVisibility(INVISIBLE);
198                 ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() {
199                     @Override
200                     public void run() {
201                         UiUtils.revealOrHideViewWithAnimation(mCloseButton, VISIBLE,
202                                 null /* onFinishRunnable */);
203                     }
204                 }, UiUtils.MEDIAPICKER_TRANSITION_DURATION + CLOSE_BUTTON_REVEAL_STAGGER_MILLIS);
205             }
206         }
207 
208         // Merge the pending attachment list with real attachment.  Design would prefer these be
209         // in LIFO order user can see added images past the 5th one but we also want them to be in
210         // order and we want it to be WYSIWYG.
211         final List<MessagePartData> combinedAttachments = new ArrayList<>();
212         combinedAttachments.addAll(attachments);
213         combinedAttachments.addAll(pendingAttachments);
214 
215         final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
216         if (combinedAttachmentCount > 1) {
217             MultiAttachmentLayout multiAttachmentLayout = null;
218             Rect transitionRect = null;
219             if (mAttachmentView.getChildCount() > 0) {
220                 final View firstChild = mAttachmentView.getChildAt(0);
221                 if (firstChild instanceof MultiAttachmentLayout) {
222                     Assert.equals(1, mAttachmentView.getChildCount());
223                     multiAttachmentLayout = (MultiAttachmentLayout) firstChild;
224                     multiAttachmentLayout.bindAttachments(combinedAttachments,
225                             null /* transitionRect */, combinedAttachmentCount);
226                 } else {
227                     transitionRect = new Rect(firstChild.getLeft(), firstChild.getTop(),
228                             firstChild.getRight(), firstChild.getBottom());
229                 }
230             }
231             if (multiAttachmentLayout == null) {
232                 multiAttachmentLayout = AttachmentPreviewFactory.createMultiplePreview(
233                         getContext(), this);
234                 multiAttachmentLayout.bindAttachments(combinedAttachments, transitionRect,
235                         combinedAttachmentCount);
236                 mAttachmentView.removeAllViews();
237                 mAttachmentView.addView(multiAttachmentLayout);
238             }
239         } else {
240             final MessagePartData attachment = combinedAttachments.get(0);
241             boolean shouldAnimate = true;
242             if (mAttachmentView.getChildCount() > 0) {
243                 // If we are going from N->1 attachments, try to use the current bounds
244                 // bounds as the starting rect.
245                 shouldAnimate = false;
246                 final View firstChild = mAttachmentView.getChildAt(0);
247                 if (firstChild instanceof MultiAttachmentLayout &&
248                         attachment instanceof MediaPickerMessagePartData) {
249                     final View leftoverView = ((MultiAttachmentLayout) firstChild)
250                             .findViewForAttachment(attachment);
251                     if (leftoverView != null) {
252                         final Rect currentRect = UiUtils.getMeasuredBoundsOnScreen(leftoverView);
253                         if (!currentRect.isEmpty() &&
254                                 attachment instanceof MediaPickerMessagePartData) {
255                             ((MediaPickerMessagePartData) attachment).setStartRect(currentRect);
256                             shouldAnimate = true;
257                         }
258                     }
259                 }
260             }
261             mAttachmentView.removeAllViews();
262             final View attachmentView = AttachmentPreviewFactory.createAttachmentPreview(
263                     layoutInflater, attachment, mAttachmentView,
264                     AttachmentPreviewFactory.TYPE_SINGLE, true /* startImageRequest */, this);
265             if (attachmentView != null) {
266                 mAttachmentView.addView(attachmentView);
267                 if (shouldAnimate) {
268                     tryAnimateViewIn(attachment, attachmentView);
269                 }
270             }
271         }
272         return true;
273     }
274 
onMessageAnimationStart()275     public void onMessageAnimationStart() {
276         if (mHideRunnable == null) {
277             return;
278         }
279 
280         // Run the hide animation at the same time as the message animation
281         mHandler.removeCallbacks(mHideRunnable);
282         setVisibility(View.INVISIBLE);
283         mHideRunnable.run();
284     }
285 
tryAnimateViewIn(final MessagePartData attachmentData, final View view)286     private void tryAnimateViewIn(final MessagePartData attachmentData, final View view) {
287         if (attachmentData instanceof MediaPickerMessagePartData) {
288             final Rect startRect = ((MediaPickerMessagePartData) attachmentData).getStartRect();
289             stopPopupAnimation();
290             mPopupTransitionAnimation = new PopupTransitionAnimation(startRect, view);
291             mPopupTransitionAnimation.startAfterLayoutComplete();
292         }
293     }
294 
stopPopupAnimation()295     private void stopPopupAnimation() {
296         if (mPopupTransitionAnimation != null) {
297             mPopupTransitionAnimation.cancel();
298             mPopupTransitionAnimation = null;
299         }
300     }
301 
302     @VisibleForAnimation
setAnimatedHeight(final int animatedHeight)303     public void setAnimatedHeight(final int animatedHeight) {
304         if (mAnimatedHeight != animatedHeight) {
305             mAnimatedHeight = animatedHeight;
306             requestLayout();
307         }
308     }
309 
310     /**
311      * Kicks off an animation to animate the layout change for closing the gap between the
312      * message list and the compose message box when the attachments are cleared.
313      */
startCloseGapAnimationOnAttachmentClear()314     private void startCloseGapAnimationOnAttachmentClear() {
315         // Cancel existing animation.
316         cancelCloseGapAnimation();
317         mCloseGapAnimator = ObjectAnimator.ofInt(this, "animatedHeight", getHeight(), 0);
318         mCloseGapAnimator.start();
319     }
320 
cancelCloseGapAnimation()321     private void cancelCloseGapAnimation() {
322         if (mCloseGapAnimator != null) {
323             mCloseGapAnimator.cancel();
324             mCloseGapAnimator = null;
325         }
326         mAnimatedHeight = -1;
327     }
328 
329     @Override
onAttachmentClick(final MessagePartData attachment, final Rect viewBoundsOnScreen, final boolean longPress)330     public boolean onAttachmentClick(final MessagePartData attachment,
331             final Rect viewBoundsOnScreen, final boolean longPress) {
332         if (longPress) {
333             mComposeMessageView.onAttachmentPreviewLongClicked();
334             return true;
335         }
336 
337         if (!(attachment instanceof PendingAttachmentData) && attachment.isImage()) {
338             mComposeMessageView.displayPhoto(attachment.getContentUri(), viewBoundsOnScreen);
339             return true;
340         }
341         return false;
342     }
343 }
344