| /* |
| * 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; |
| } |
| } |
| } |