/* * 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 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 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(); } public void bindAttachments(final Iterable attachments, final Rect transitionRect, final int count) { final ArrayList previousViews = mPreviewViews; mPreviewViews = new ArrayList(); 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 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 attachments, final ArrayList previousViews, final Rect transitionRect) { final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); final int count = mCurrentLayout.tiles.size(); int i = 0; final Iterator 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; } } }