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 
AttachmentPreview(final Context context, final AttributeSet attrs)62     public AttachmentPreview(final Context context, final AttributeSet attrs) {
63         super(context, attrs);
64         mHandler = new Handler(Looper.getMainLooper());
65     }
66 
67     @Override
onFinishInflate()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 
setComposeMessageView(final ComposeMessageView composeMessageView)103     public void setComposeMessageView(final ComposeMessageView composeMessageView) {
104         mComposeMessageView = composeMessageView;
105     }
106 
107     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)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 
cancelPendingHide()115     private void cancelPendingHide() {
116         mPendingHideCanceled = true;
117     }
118 
hideAttachmentPreview()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
onAttachmentsChanged(final DraftMessageData draftMessageData)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 
onMessageAnimationStart()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 
tryAnimateViewIn(final MessagePartData attachmentData, final View view)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
setAnimatedHeight(final int animatedHeight)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      */
startCloseGapAnimationOnAttachmentClear()302     private void startCloseGapAnimationOnAttachmentClear() {
303         // Cancel existing animation.
304         cancelCloseGapAnimation();
305         mCloseGapAnimator = ObjectAnimator.ofInt(this, "animatedHeight", getHeight(), 0);
306         mCloseGapAnimator.start();
307     }
308 
cancelCloseGapAnimation()309     private void cancelCloseGapAnimation() {
310         if (mCloseGapAnimator != null) {
311             mCloseGapAnimator.cancel();
312             mCloseGapAnimator = null;
313         }
314         mAnimatedHeight = -1;
315     }
316 
317     @Override
onAttachmentClick(final MessagePartData attachment, final Rect viewBoundsOnScreen, final boolean longPress)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