summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/MultiAttachmentLayout.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/ui/MultiAttachmentLayout.java')
-rw-r--r--src/com/android/messaging/ui/MultiAttachmentLayout.java424
1 files changed, 424 insertions, 0 deletions
diff --git a/src/com/android/messaging/ui/MultiAttachmentLayout.java b/src/com/android/messaging/ui/MultiAttachmentLayout.java
new file mode 100644
index 0000000..f620245
--- /dev/null
+++ b/src/com/android/messaging/ui/MultiAttachmentLayout.java
@@ -0,0 +1,424 @@
+/*
+ * 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.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) {
+ AttachmentPreview.tryAnimateViewIn(attachment, attachmentWrapper.view);
+ }
+ 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;
+ }
+ }
+}