Stop attaching animation before removal attachment views and hiding the
AttachmentPreview.
Test: Long-press to attach a media from GalleryGridView and uncheck the
media to detach immediately.
Change-Id: Ia1014609c056c9f35e428f8968b58dc2c1b704f9
Signed-off-by: Taesu Lee <taesu82.lee@samsung.com>
430 lines
18 KiB
Java
430 lines
18 KiB
Java
/*
|
|
* 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.content.Context;
|
|
import android.graphics.Rect;
|
|
import android.util.AttributeSet;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.animation.AnimationSet;
|
|
import android.view.animation.ScaleAnimation;
|
|
import android.view.animation.TranslateAnimation;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.messaging.R;
|
|
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.datamodel.media.ImageRequestDescriptor;
|
|
import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
|
|
import com.android.messaging.ui.animation.PopupTransitionAnimation;
|
|
import com.android.messaging.util.AccessibilityUtil;
|
|
import com.android.messaging.util.Assert;
|
|
import com.android.messaging.util.UiUtils;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Holds and displays multiple attachments in a 4x2 grid. Each preview image "tile" can take
|
|
* one of three sizes - small (1x1), wide (2x1) and large (2x2). We have a number of predefined
|
|
* layout settings designed for holding 2, 3, 4+ attachments (these layout settings are
|
|
* tweakable by design request to allow for max flexibility). For a visual example, consider the
|
|
* following attachment layout:
|
|
*
|
|
* +---------------+----------------+
|
|
* | | |
|
|
* | | B |
|
|
* | | |
|
|
* | A |-------+--------|
|
|
* | | | |
|
|
* | | C | D |
|
|
* | | | |
|
|
* +---------------+-------+--------+
|
|
*
|
|
* In the above example, the layout consists of four tiles, A-D. A is a large tile, B is a
|
|
* wide tile and C & D are both small tiles. A starts at (0,0) and ends at (1,1), B starts at
|
|
* (2,0) and ends at (3,0), and so on. In our layout class we'd have these tiles in the order
|
|
* of A-D, so that we make sure the last tile is always the one where we can put the overflow
|
|
* indicator (e.g. "+2").
|
|
*/
|
|
public class MultiAttachmentLayout extends FrameLayout {
|
|
|
|
public interface OnAttachmentClickListener {
|
|
boolean onAttachmentClick(MessagePartData attachment, Rect viewBoundsOnScreen,
|
|
boolean longPress);
|
|
}
|
|
|
|
private static final int GRID_WIDTH = 4; // in # of cells
|
|
private static final int GRID_HEIGHT = 2; // in # of cells
|
|
|
|
/**
|
|
* Represents a preview image tile in the layout
|
|
*/
|
|
private static class Tile {
|
|
public final int startX;
|
|
public final int startY;
|
|
public final int endX;
|
|
public final int endY;
|
|
|
|
private Tile(final int startX, final int startY, final int endX, final int endY) {
|
|
this.startX = startX;
|
|
this.startY = startY;
|
|
this.endX = endX;
|
|
this.endY = endY;
|
|
}
|
|
|
|
public int getWidthMeasureSpec(final int cellWidth, final int padding) {
|
|
return MeasureSpec.makeMeasureSpec((endX - startX + 1) * cellWidth - padding * 2,
|
|
MeasureSpec.EXACTLY);
|
|
}
|
|
|
|
public int getHeightMeasureSpec(final int cellHeight, final int padding) {
|
|
return MeasureSpec.makeMeasureSpec((endY - startY + 1) * cellHeight - padding * 2,
|
|
MeasureSpec.EXACTLY);
|
|
}
|
|
|
|
public static Tile large(final int startX, final int startY) {
|
|
return new Tile(startX, startY, startX + 1, startY + 1);
|
|
}
|
|
|
|
public static Tile wide(final int startX, final int startY) {
|
|
return new Tile(startX, startY, startX + 1, startY);
|
|
}
|
|
|
|
public static Tile small(final int startX, final int startY) {
|
|
return new Tile(startX, startY, startX, startY);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A layout simply contains a list of tiles, in the order of top-left -> bottom-right.
|
|
*/
|
|
private static class Layout {
|
|
public final List<Tile> tiles;
|
|
public Layout(final Tile[] tilesArray) {
|
|
tiles = Arrays.asList(tilesArray);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List of predefined layout configurations w.r.t no. of attachments.
|
|
*/
|
|
private static final Layout[] ATTACHMENT_LAYOUTS_BY_COUNT = {
|
|
null, // Doesn't support zero attachments.
|
|
null, // Doesn't support one attachment. Single attachment preview is used instead.
|
|
new Layout(new Tile[] { Tile.large(0, 0), Tile.large(2, 0) }), // 2 items
|
|
new Layout(new Tile[] { Tile.large(0, 0), Tile.wide(2, 0), Tile.wide(2, 1) }), // 3 items
|
|
new Layout(new Tile[] { Tile.large(0, 0), Tile.wide(2, 0), Tile.small(2, 1), // 4+ items
|
|
Tile.small(3, 1) }),
|
|
};
|
|
|
|
/**
|
|
* List of predefined RTL layout configurations w.r.t no. of attachments.
|
|
*/
|
|
private static final Layout[] ATTACHMENT_RTL_LAYOUTS_BY_COUNT = {
|
|
null, // Doesn't support zero attachments.
|
|
null, // Doesn't support one attachment. Single attachment preview is used instead.
|
|
new Layout(new Tile[] { Tile.large(2, 0), Tile.large(0, 0)}), // 2 items
|
|
new Layout(new Tile[] { Tile.large(2, 0), Tile.wide(0, 0), Tile.wide(0, 1) }), // 3 items
|
|
new Layout(new Tile[] { Tile.large(2, 0), Tile.wide(0, 0), Tile.small(1, 1), // 4+ items
|
|
Tile.small(0, 1) }),
|
|
};
|
|
|
|
private Layout mCurrentLayout;
|
|
private ArrayList<ViewWrapper> mPreviewViews;
|
|
private int mPlusNumber;
|
|
private TextView mPlusTextView;
|
|
private OnAttachmentClickListener mAttachmentClickListener;
|
|
private AsyncImageViewDelayLoader mImageViewDelayLoader;
|
|
|
|
public MultiAttachmentLayout(final Context context, final AttributeSet attrs) {
|
|
super(context, attrs);
|
|
mPreviewViews = new ArrayList<ViewWrapper>();
|
|
}
|
|
|
|
public void bindAttachments(final Iterable<MessagePartData> attachments,
|
|
final Rect transitionRect, final int count) {
|
|
final ArrayList<ViewWrapper> previousViews = mPreviewViews;
|
|
mPreviewViews = new ArrayList<ViewWrapper>();
|
|
removeView(mPlusTextView);
|
|
mPlusTextView = null;
|
|
|
|
determineLayout(attachments, count);
|
|
buildViews(attachments, previousViews, transitionRect);
|
|
|
|
// Remove all previous views that couldn't be recycled.
|
|
for (final ViewWrapper viewWrapper : previousViews) {
|
|
removeView(viewWrapper.view);
|
|
}
|
|
requestLayout();
|
|
}
|
|
|
|
public OnAttachmentClickListener getOnAttachmentClickListener() {
|
|
return mAttachmentClickListener;
|
|
}
|
|
|
|
public void setOnAttachmentClickListener(final OnAttachmentClickListener listener) {
|
|
mAttachmentClickListener = listener;
|
|
}
|
|
|
|
public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) {
|
|
mImageViewDelayLoader = delayLoader;
|
|
}
|
|
|
|
public void setColorFilter(int color) {
|
|
for (ViewWrapper viewWrapper : mPreviewViews) {
|
|
if (viewWrapper.view instanceof AsyncImageView) {
|
|
((AsyncImageView) viewWrapper.view).setColorFilter(color);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void clearColorFilter() {
|
|
for (ViewWrapper viewWrapper : mPreviewViews) {
|
|
if (viewWrapper.view instanceof AsyncImageView) {
|
|
((AsyncImageView) viewWrapper.view).clearColorFilter();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void determineLayout(final Iterable<MessagePartData> attachments, final int count) {
|
|
Assert.isTrue(attachments != null);
|
|
final boolean isRtl = AccessibilityUtil.isLayoutRtl(getRootView());
|
|
if (isRtl) {
|
|
mCurrentLayout = ATTACHMENT_RTL_LAYOUTS_BY_COUNT[Math.min(count,
|
|
ATTACHMENT_RTL_LAYOUTS_BY_COUNT.length - 1)];
|
|
} else {
|
|
mCurrentLayout = ATTACHMENT_LAYOUTS_BY_COUNT[Math.min(count,
|
|
ATTACHMENT_LAYOUTS_BY_COUNT.length - 1)];
|
|
}
|
|
|
|
// We must have a valid layout for the current configuration.
|
|
Assert.notNull(mCurrentLayout);
|
|
|
|
mPlusNumber = count - mCurrentLayout.tiles.size();
|
|
Assert.isTrue(mPlusNumber >= 0);
|
|
}
|
|
|
|
private void buildViews(final Iterable<MessagePartData> attachments,
|
|
final ArrayList<ViewWrapper> previousViews, final Rect transitionRect) {
|
|
final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
|
|
final int count = mCurrentLayout.tiles.size();
|
|
int i = 0;
|
|
final Iterator<MessagePartData> iterator = attachments.iterator();
|
|
while (iterator.hasNext() && i < count) {
|
|
final MessagePartData attachment = iterator.next();
|
|
ViewWrapper attachmentWrapper = null;
|
|
// Try to recycle a previous view first
|
|
for (int j = 0; j < previousViews.size(); j++) {
|
|
final ViewWrapper previousView = previousViews.get(j);
|
|
if (previousView.attachment.equals(attachment) &&
|
|
!(previousView.attachment instanceof PendingAttachmentData)) {
|
|
attachmentWrapper = previousView;
|
|
previousViews.remove(j);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (attachmentWrapper == null) {
|
|
final View view = AttachmentPreviewFactory.createAttachmentPreview(layoutInflater,
|
|
attachment, this, AttachmentPreviewFactory.TYPE_MULTIPLE,
|
|
false /* startImageRequest */, mAttachmentClickListener);
|
|
|
|
if (view == null) {
|
|
// createAttachmentPreview can return null if something goes wrong (e.g.
|
|
// attachment has unsupported contentType)
|
|
continue;
|
|
}
|
|
if (view instanceof AsyncImageView && mImageViewDelayLoader != null) {
|
|
AsyncImageView asyncImageView = (AsyncImageView) view;
|
|
asyncImageView.setDelayLoader(mImageViewDelayLoader);
|
|
}
|
|
addView(view);
|
|
attachmentWrapper = new ViewWrapper(view, attachment);
|
|
// Help animate from single to multi by copying over the prev location
|
|
if (count == 2 && i == 1 && transitionRect != null) {
|
|
attachmentWrapper.prevLeft = transitionRect.left;
|
|
attachmentWrapper.prevTop = transitionRect.top;
|
|
attachmentWrapper.prevWidth = transitionRect.width();
|
|
attachmentWrapper.prevHeight = transitionRect.height();
|
|
}
|
|
}
|
|
i++;
|
|
Assert.notNull(attachmentWrapper);
|
|
mPreviewViews.add(attachmentWrapper);
|
|
|
|
// The first view will animate in using PopupTransitionAnimation, but the remaining
|
|
// views will slide from their previous position to their new position within the
|
|
// layout
|
|
if (i == 0) {
|
|
if (attachment instanceof MediaPickerMessagePartData) {
|
|
final Rect startRect = ((MediaPickerMessagePartData) attachment).getStartRect();
|
|
new PopupTransitionAnimation(startRect, attachmentWrapper.view)
|
|
.startAfterLayoutComplete();
|
|
}
|
|
}
|
|
attachmentWrapper.needsSlideAnimation = i > 0;
|
|
}
|
|
|
|
// Build the plus text view (e.g. "+2") for when there are more attachments than what
|
|
// this layout can display.
|
|
if (mPlusNumber > 0) {
|
|
mPlusTextView = (TextView) layoutInflater.inflate(R.layout.attachment_more_text_view,
|
|
null /* parent */);
|
|
mPlusTextView.setText(getResources().getString(R.string.attachment_more_items,
|
|
mPlusNumber));
|
|
addView(mPlusTextView);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
|
final int maxWidth = getResources().getDimensionPixelSize(
|
|
R.dimen.multiple_attachment_preview_width);
|
|
final int maxHeight = getResources().getDimensionPixelSize(
|
|
R.dimen.multiple_attachment_preview_height);
|
|
final int width = Math.min(MeasureSpec.getSize(widthMeasureSpec), maxWidth);
|
|
final int height = maxHeight;
|
|
final int cellWidth = width / GRID_WIDTH;
|
|
final int cellHeight = height / GRID_HEIGHT;
|
|
final int count = mPreviewViews.size();
|
|
final int padding = getResources().getDimensionPixelOffset(
|
|
R.dimen.multiple_attachment_preview_padding);
|
|
for (int i = 0; i < count; i++) {
|
|
final View view = mPreviewViews.get(i).view;
|
|
final Tile imageTile = mCurrentLayout.tiles.get(i);
|
|
view.measure(imageTile.getWidthMeasureSpec(cellWidth, padding),
|
|
imageTile.getHeightMeasureSpec(cellHeight, padding));
|
|
|
|
// Now that we know the size, we can request an appropriately-sized image.
|
|
if (view instanceof AsyncImageView) {
|
|
final ImageRequestDescriptor imageRequest =
|
|
AttachmentPreviewFactory.getImageRequestDescriptorForAttachment(
|
|
mPreviewViews.get(i).attachment,
|
|
view.getMeasuredWidth(),
|
|
view.getMeasuredHeight());
|
|
((AsyncImageView) view).setImageResourceId(imageRequest);
|
|
}
|
|
|
|
if (i == count - 1 && mPlusTextView != null) {
|
|
// The plus text view always covers the last attachment.
|
|
mPlusTextView.measure(imageTile.getWidthMeasureSpec(cellWidth, padding),
|
|
imageTile.getHeightMeasureSpec(cellHeight, padding));
|
|
}
|
|
}
|
|
setMeasuredDimension(width, height);
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(final boolean changed, final int left, final int top, final int right,
|
|
final int bottom) {
|
|
final int cellWidth = getMeasuredWidth() / GRID_WIDTH;
|
|
final int cellHeight = getMeasuredHeight() / GRID_HEIGHT;
|
|
final int padding = getResources().getDimensionPixelOffset(
|
|
R.dimen.multiple_attachment_preview_padding);
|
|
final int count = mPreviewViews.size();
|
|
for (int i = 0; i < count; i++) {
|
|
final ViewWrapper viewWrapper = mPreviewViews.get(i);
|
|
final View view = viewWrapper.view;
|
|
final Tile imageTile = mCurrentLayout.tiles.get(i);
|
|
final int tileLeft = imageTile.startX * cellWidth;
|
|
final int tileTop = imageTile.startY * cellHeight;
|
|
view.layout(tileLeft + padding, tileTop + padding,
|
|
tileLeft + view.getMeasuredWidth(),
|
|
tileTop + view.getMeasuredHeight());
|
|
if (viewWrapper.needsSlideAnimation) {
|
|
trySlideAttachmentView(viewWrapper);
|
|
viewWrapper.needsSlideAnimation = false;
|
|
} else {
|
|
viewWrapper.prevLeft = view.getLeft();
|
|
viewWrapper.prevTop = view.getTop();
|
|
viewWrapper.prevWidth = view.getWidth();
|
|
viewWrapper.prevHeight = view.getHeight();
|
|
}
|
|
|
|
if (i == count - 1 && mPlusTextView != null) {
|
|
// The plus text view always covers the last attachment.
|
|
mPlusTextView.layout(tileLeft + padding, tileTop + padding,
|
|
tileLeft + mPlusTextView.getMeasuredWidth(),
|
|
tileTop + mPlusTextView.getMeasuredHeight());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void trySlideAttachmentView(final ViewWrapper viewWrapper) {
|
|
if (!(viewWrapper.attachment instanceof MediaPickerMessagePartData)) {
|
|
return;
|
|
}
|
|
final View view = viewWrapper.view;
|
|
|
|
|
|
final int xOffset = viewWrapper.prevLeft - view.getLeft();
|
|
final int yOffset = viewWrapper.prevTop - view.getTop();
|
|
final float scaleX = viewWrapper.prevWidth / (float) view.getWidth();
|
|
final float scaleY = viewWrapper.prevHeight / (float) view.getHeight();
|
|
|
|
if (xOffset == 0 && yOffset == 0 && scaleX == 1 && scaleY == 1) {
|
|
// Layout hasn't changed
|
|
return;
|
|
}
|
|
|
|
final AnimationSet animationSet = new AnimationSet(
|
|
true /* shareInterpolator */);
|
|
animationSet.addAnimation(new TranslateAnimation(xOffset, 0, yOffset, 0));
|
|
animationSet.addAnimation(new ScaleAnimation(scaleX, 1, scaleY, 1));
|
|
animationSet.setDuration(
|
|
UiUtils.MEDIAPICKER_TRANSITION_DURATION);
|
|
animationSet.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
|
|
view.startAnimation(animationSet);
|
|
view.invalidate();
|
|
viewWrapper.prevLeft = view.getLeft();
|
|
viewWrapper.prevTop = view.getTop();
|
|
viewWrapper.prevWidth = view.getWidth();
|
|
viewWrapper.prevHeight = view.getHeight();
|
|
}
|
|
|
|
public View findViewForAttachment(final MessagePartData attachment) {
|
|
for (ViewWrapper wrapper : mPreviewViews) {
|
|
if (wrapper.attachment.equals(attachment) &&
|
|
!(wrapper.attachment instanceof PendingAttachmentData)) {
|
|
return wrapper.view;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static class ViewWrapper {
|
|
final View view;
|
|
final MessagePartData attachment;
|
|
boolean needsSlideAnimation;
|
|
int prevLeft;
|
|
int prevTop;
|
|
int prevWidth;
|
|
int prevHeight;
|
|
|
|
ViewWrapper(final View view, final MessagePartData attachment) {
|
|
this.view = view;
|
|
this.attachment = attachment;
|
|
}
|
|
}
|
|
}
|