blob: f4465c4649b9192a194c05da25573d827514820d [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ScrollView;
import com.android.messaging.R;
import com.android.messaging.annotation.VisibleForAnimation;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener;
import com.android.messaging.ui.animation.PopupTransitionAnimation;
import com.android.messaging.ui.conversation.ComposeMessageView;
import com.android.messaging.ui.conversation.ConversationFragment;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ThreadUtil;
import com.android.messaging.util.UiUtils;
import java.util.ArrayList;
import java.util.List;
public class AttachmentPreview extends ScrollView implements OnAttachmentClickListener {
private FrameLayout mAttachmentView;
private ComposeMessageView mComposeMessageView;
private ImageButton mCloseButton;
private int mAnimatedHeight = -1;
private Animator mCloseGapAnimator;
private boolean mPendingFirstUpdate;
private Handler mHandler;
private Runnable mHideRunnable;
private boolean mPendingHideCanceled;
private PopupTransitionAnimation mPopupTransitionAnimation;
private static final int CLOSE_BUTTON_REVEAL_STAGGER_MILLIS = 300;
public AttachmentPreview(final Context context, final AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(Looper.getMainLooper());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mCloseButton = (ImageButton) findViewById(R.id.close_button);
mCloseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View view) {
mComposeMessageView.clearAttachments();
}
});
mAttachmentView = (FrameLayout) findViewById(R.id.attachment_view);
// The attachment preview is a scroll view so that it can show the bottom portion of the
// attachment whenever the space is tight (e.g. when in landscape mode). Per design
// request we'd like to make the attachment view always scrolled to the bottom.
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(final View v, final int left, final int top, final int right,
final int bottom, final int oldLeft, final int oldTop, final int oldRight,
final int oldBottom) {
post(new Runnable() {
@Override
public void run() {
final int childCount = getChildCount();
if (childCount > 0) {
final View lastChild = getChildAt(childCount - 1);
scrollTo(getScrollX(), lastChild.getBottom() - getHeight());
}
}
});
}
});
mPendingFirstUpdate = true;
}
public void setComposeMessageView(final ComposeMessageView composeMessageView) {
mComposeMessageView = composeMessageView;
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mAnimatedHeight >= 0) {
setMeasuredDimension(getMeasuredWidth(), mAnimatedHeight);
}
}
private void cancelPendingHide() {
mPendingHideCanceled = true;
}
public void hideAttachmentPreview() {
if (getVisibility() != GONE) {
UiUtils.revealOrHideViewWithAnimation(mCloseButton, GONE,
null /* onFinishRunnable */);
startCloseGapAnimationOnAttachmentClear();
if (mAttachmentView.getChildCount() > 0) {
mPendingHideCanceled = false;
final View viewToHide = mAttachmentView.getChildCount() > 1 ?
mAttachmentView : mAttachmentView.getChildAt(0);
UiUtils.revealOrHideViewWithAnimation(viewToHide, INVISIBLE,
new Runnable() {
@Override
public void run() {
// Only hide if we are didn't get overruled by showing
if (!mPendingHideCanceled) {
stopPopupAnimation();
mAttachmentView.removeAllViews();
setVisibility(GONE);
}
}
});
} else {
mAttachmentView.removeAllViews();
setVisibility(GONE);
}
}
}
// returns true if we have attachments
public boolean onAttachmentsChanged(final DraftMessageData draftMessageData) {
final boolean isFirstUpdate = mPendingFirstUpdate;
final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments();
final List<PendingAttachmentData> pendingAttachments =
draftMessageData.getReadOnlyPendingAttachments();
// Any change in attachments would invalidate the animated height animation.
cancelCloseGapAnimation();
mPendingFirstUpdate = false;
final int combinedAttachmentCount = attachments.size() + pendingAttachments.size();
mCloseButton.setContentDescription(getResources()
.getQuantityString(R.plurals.attachment_preview_close_content_description,
combinedAttachmentCount));
if (combinedAttachmentCount == 0) {
mHideRunnable = new Runnable() {
@Override
public void run() {
mHideRunnable = null;
// Only start the hiding if there are still no attachments
if (attachments.size() + pendingAttachments.size() == 0) {
hideAttachmentPreview();
}
}
};
if (draftMessageData.isSending()) {
// Wait to hide until the message is ready to start animating
// We'll execute immediately when the animation triggers
mHandler.postDelayed(mHideRunnable,
ConversationFragment.MESSAGE_ANIMATION_MAX_WAIT);
} else {
// Run immediately when clearing attachments
mHideRunnable.run();
}
return false;
}
cancelPendingHide(); // We're showing
if (getVisibility() != VISIBLE) {
setVisibility(VISIBLE);
mAttachmentView.setVisibility(VISIBLE);
// Don't animate in the close button if this is the first update after view creation.
// This is the initial draft load from database for pre-existing drafts.
if (!isFirstUpdate) {
// Reveal the close button after the view animates in.
mCloseButton.setVisibility(INVISIBLE);
ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() {
@Override
public void run() {
UiUtils.revealOrHideViewWithAnimation(mCloseButton, VISIBLE,
null /* onFinishRunnable */);
}
}, UiUtils.MEDIAPICKER_TRANSITION_DURATION + CLOSE_BUTTON_REVEAL_STAGGER_MILLIS);
}
}
// Merge the pending attachment list with real attachment. Design would prefer these be
// in LIFO order user can see added images past the 5th one but we also want them to be in
// order and we want it to be WYSIWYG.
final List<MessagePartData> combinedAttachments = new ArrayList<>();
combinedAttachments.addAll(attachments);
combinedAttachments.addAll(pendingAttachments);
final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
if (combinedAttachmentCount > 1) {
MultiAttachmentLayout multiAttachmentLayout = null;
Rect transitionRect = null;
if (mAttachmentView.getChildCount() > 0) {
final View firstChild = mAttachmentView.getChildAt(0);
if (firstChild instanceof MultiAttachmentLayout) {
Assert.equals(1, mAttachmentView.getChildCount());
multiAttachmentLayout = (MultiAttachmentLayout) firstChild;
multiAttachmentLayout.bindAttachments(combinedAttachments,
null /* transitionRect */, combinedAttachmentCount);
} else {
transitionRect = new Rect(firstChild.getLeft(), firstChild.getTop(),
firstChild.getRight(), firstChild.getBottom());
}
}
if (multiAttachmentLayout == null) {
multiAttachmentLayout = AttachmentPreviewFactory.createMultiplePreview(
getContext(), this);
multiAttachmentLayout.bindAttachments(combinedAttachments, transitionRect,
combinedAttachmentCount);
mAttachmentView.removeAllViews();
mAttachmentView.addView(multiAttachmentLayout);
}
} else {
final MessagePartData attachment = combinedAttachments.get(0);
boolean shouldAnimate = true;
if (mAttachmentView.getChildCount() > 0) {
// If we are going from N->1 attachments, try to use the current bounds
// bounds as the starting rect.
shouldAnimate = false;
final View firstChild = mAttachmentView.getChildAt(0);
if (firstChild instanceof MultiAttachmentLayout &&
attachment instanceof MediaPickerMessagePartData) {
final View leftoverView = ((MultiAttachmentLayout) firstChild)
.findViewForAttachment(attachment);
if (leftoverView != null) {
final Rect currentRect = UiUtils.getMeasuredBoundsOnScreen(leftoverView);
if (!currentRect.isEmpty() &&
attachment instanceof MediaPickerMessagePartData) {
((MediaPickerMessagePartData) attachment).setStartRect(currentRect);
shouldAnimate = true;
}
}
}
}
mAttachmentView.removeAllViews();
final View attachmentView = AttachmentPreviewFactory.createAttachmentPreview(
layoutInflater, attachment, mAttachmentView,
AttachmentPreviewFactory.TYPE_SINGLE, true /* startImageRequest */, this);
if (attachmentView != null) {
mAttachmentView.addView(attachmentView);
if (shouldAnimate) {
tryAnimateViewIn(attachment, attachmentView);
}
}
}
return true;
}
public void onMessageAnimationStart() {
if (mHideRunnable == null) {
return;
}
// Run the hide animation at the same time as the message animation
mHandler.removeCallbacks(mHideRunnable);
setVisibility(View.INVISIBLE);
mHideRunnable.run();
}
private void tryAnimateViewIn(final MessagePartData attachmentData, final View view) {
if (attachmentData instanceof MediaPickerMessagePartData) {
final Rect startRect = ((MediaPickerMessagePartData) attachmentData).getStartRect();
stopPopupAnimation();
mPopupTransitionAnimation = new PopupTransitionAnimation(startRect, view);
mPopupTransitionAnimation.startAfterLayoutComplete();
}
}
private void stopPopupAnimation() {
if (mPopupTransitionAnimation != null) {
mPopupTransitionAnimation.cancel();
mPopupTransitionAnimation = null;
}
}
@VisibleForAnimation
public void setAnimatedHeight(final int animatedHeight) {
if (mAnimatedHeight != animatedHeight) {
mAnimatedHeight = animatedHeight;
requestLayout();
}
}
/**
* Kicks off an animation to animate the layout change for closing the gap between the
* message list and the compose message box when the attachments are cleared.
*/
private void startCloseGapAnimationOnAttachmentClear() {
// Cancel existing animation.
cancelCloseGapAnimation();
mCloseGapAnimator = ObjectAnimator.ofInt(this, "animatedHeight", getHeight(), 0);
mCloseGapAnimator.start();
}
private void cancelCloseGapAnimation() {
if (mCloseGapAnimator != null) {
mCloseGapAnimator.cancel();
mCloseGapAnimator = null;
}
mAnimatedHeight = -1;
}
@Override
public boolean onAttachmentClick(final MessagePartData attachment,
final Rect viewBoundsOnScreen, final boolean longPress) {
if (longPress) {
mComposeMessageView.onAttachmentPreviewLongClicked();
return true;
}
if (!(attachment instanceof PendingAttachmentData) && attachment.isImage()) {
mComposeMessageView.displayPhoto(attachment.getContentUri(), viewBoundsOnScreen);
return true;
}
return false;
}
}