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