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