1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.messaging.ui;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.util.AttributeSet;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.animation.AnimationSet;
25 import android.view.animation.ScaleAnimation;
26 import android.view.animation.TranslateAnimation;
27 import android.widget.FrameLayout;
28 import android.widget.TextView;
29 
30 import com.android.messaging.R;
31 import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
32 import com.android.messaging.datamodel.data.MessagePartData;
33 import com.android.messaging.datamodel.data.PendingAttachmentData;
34 import com.android.messaging.datamodel.media.ImageRequestDescriptor;
35 import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
36 import com.android.messaging.util.AccessibilityUtil;
37 import com.android.messaging.util.Assert;
38 import com.android.messaging.util.UiUtils;
39 
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Iterator;
43 import java.util.List;
44 
45 /**
46  * Holds and displays multiple attachments in a 4x2 grid. Each preview image "tile" can take
47  * one of three sizes - small (1x1), wide (2x1) and large (2x2). We have a number of predefined
48  * layout settings designed for holding 2, 3, 4+ attachments (these layout settings are
49  * tweakable by design request to allow for max flexibility). For a visual example, consider the
50  * following attachment layout:
51  *
52  * +---------------+----------------+
53  * |               |                |
54  * |               |       B        |
55  * |               |                |
56  * |       A       |-------+--------|
57  * |               |       |        |
58  * |               |   C   |    D   |
59  * |               |       |        |
60  * +---------------+-------+--------+
61  *
62  * In the above example, the layout consists of four tiles, A-D. A is a large tile, B is a
63  * wide tile and C & D are both small tiles. A starts at (0,0) and ends at (1,1), B starts at
64  * (2,0) and ends at (3,0), and so on. In our layout class we'd have these tiles in the order
65  * of A-D, so that we make sure the last tile is always the one where we can put the overflow
66  * indicator (e.g. "+2").
67  */
68 public class MultiAttachmentLayout extends FrameLayout {
69 
70     public interface OnAttachmentClickListener {
onAttachmentClick(MessagePartData attachment, Rect viewBoundsOnScreen, boolean longPress)71         boolean onAttachmentClick(MessagePartData attachment, Rect viewBoundsOnScreen,
72                 boolean longPress);
73     }
74 
75     private static final int GRID_WIDTH = 4;    // in # of cells
76     private static final int GRID_HEIGHT = 2;   // in # of cells
77 
78     /**
79      * Represents a preview image tile in the layout
80      */
81     private static class Tile {
82         public final int startX;
83         public final int startY;
84         public final int endX;
85         public final int endY;
86 
Tile(final int startX, final int startY, final int endX, final int endY)87         private Tile(final int startX, final int startY, final int endX, final int endY) {
88             this.startX = startX;
89             this.startY = startY;
90             this.endX = endX;
91             this.endY = endY;
92         }
93 
getWidthMeasureSpec(final int cellWidth, final int padding)94         public int getWidthMeasureSpec(final int cellWidth, final int padding) {
95             return MeasureSpec.makeMeasureSpec((endX - startX + 1) * cellWidth - padding * 2,
96                     MeasureSpec.EXACTLY);
97         }
98 
getHeightMeasureSpec(final int cellHeight, final int padding)99         public int getHeightMeasureSpec(final int cellHeight, final int padding) {
100             return MeasureSpec.makeMeasureSpec((endY - startY + 1) * cellHeight - padding * 2,
101                     MeasureSpec.EXACTLY);
102         }
103 
large(final int startX, final int startY)104         public static Tile large(final int startX, final int startY) {
105             return new Tile(startX, startY, startX + 1, startY + 1);
106         }
107 
wide(final int startX, final int startY)108         public static Tile wide(final int startX, final int startY) {
109             return new Tile(startX, startY, startX + 1, startY);
110         }
111 
small(final int startX, final int startY)112         public static Tile small(final int startX, final int startY) {
113             return new Tile(startX, startY, startX, startY);
114         }
115     }
116 
117     /**
118      * A layout simply contains a list of tiles, in the order of top-left -> bottom-right.
119      */
120     private static class Layout {
121         public final List<Tile> tiles;
Layout(final Tile[] tilesArray)122         public Layout(final Tile[] tilesArray) {
123             tiles = Arrays.asList(tilesArray);
124         }
125     }
126 
127     /**
128      * List of predefined layout configurations w.r.t no. of attachments.
129      */
130     private static final Layout[] ATTACHMENT_LAYOUTS_BY_COUNT = {
131         null,   // Doesn't support zero attachments.
132         null,   // Doesn't support one attachment. Single attachment preview is used instead.
133         new Layout(new Tile[] { Tile.large(0, 0), Tile.large(2, 0) }),                  // 2 items
134         new Layout(new Tile[] { Tile.large(0, 0), Tile.wide(2, 0), Tile.wide(2, 1) }),  // 3 items
135         new Layout(new Tile[] { Tile.large(0, 0), Tile.wide(2, 0), Tile.small(2, 1),    // 4+ items
136                 Tile.small(3, 1) }),
137     };
138 
139     /**
140      * List of predefined RTL layout configurations w.r.t no. of attachments.
141      */
142     private static final Layout[] ATTACHMENT_RTL_LAYOUTS_BY_COUNT = {
143         null,   // Doesn't support zero attachments.
144         null,   // Doesn't support one attachment. Single attachment preview is used instead.
145         new Layout(new Tile[] { Tile.large(2, 0), Tile.large(0, 0)}),                   // 2 items
146         new Layout(new Tile[] { Tile.large(2, 0), Tile.wide(0, 0), Tile.wide(0, 1) }),  // 3 items
147         new Layout(new Tile[] { Tile.large(2, 0), Tile.wide(0, 0), Tile.small(1, 1),    // 4+ items
148                 Tile.small(0, 1) }),
149     };
150 
151     private Layout mCurrentLayout;
152     private ArrayList<ViewWrapper> mPreviewViews;
153     private int mPlusNumber;
154     private TextView mPlusTextView;
155     private OnAttachmentClickListener mAttachmentClickListener;
156     private AsyncImageViewDelayLoader mImageViewDelayLoader;
157 
MultiAttachmentLayout(final Context context, final AttributeSet attrs)158     public MultiAttachmentLayout(final Context context, final AttributeSet attrs) {
159         super(context, attrs);
160         mPreviewViews = new ArrayList<ViewWrapper>();
161     }
162 
bindAttachments(final Iterable<MessagePartData> attachments, final Rect transitionRect, final int count)163     public void bindAttachments(final Iterable<MessagePartData> attachments,
164             final Rect transitionRect, final int count) {
165         final ArrayList<ViewWrapper> previousViews = mPreviewViews;
166         mPreviewViews = new ArrayList<ViewWrapper>();
167         removeView(mPlusTextView);
168         mPlusTextView = null;
169 
170         determineLayout(attachments, count);
171         buildViews(attachments, previousViews, transitionRect);
172 
173         // Remove all previous views that couldn't be recycled.
174         for (final ViewWrapper viewWrapper : previousViews) {
175             removeView(viewWrapper.view);
176         }
177         requestLayout();
178     }
179 
getOnAttachmentClickListener()180     public OnAttachmentClickListener getOnAttachmentClickListener() {
181         return mAttachmentClickListener;
182     }
183 
setOnAttachmentClickListener(final OnAttachmentClickListener listener)184     public void setOnAttachmentClickListener(final OnAttachmentClickListener listener) {
185         mAttachmentClickListener = listener;
186     }
187 
setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader)188     public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) {
189         mImageViewDelayLoader = delayLoader;
190     }
191 
setColorFilter(int color)192     public void setColorFilter(int color) {
193         for (ViewWrapper viewWrapper : mPreviewViews) {
194             if (viewWrapper.view instanceof AsyncImageView) {
195                 ((AsyncImageView) viewWrapper.view).setColorFilter(color);
196             }
197         }
198     }
199 
clearColorFilter()200     public void clearColorFilter() {
201         for (ViewWrapper viewWrapper : mPreviewViews) {
202             if (viewWrapper.view instanceof AsyncImageView) {
203                 ((AsyncImageView) viewWrapper.view).clearColorFilter();
204             }
205         }
206     }
207 
determineLayout(final Iterable<MessagePartData> attachments, final int count)208     private void determineLayout(final Iterable<MessagePartData> attachments, final int count) {
209         Assert.isTrue(attachments != null);
210         final boolean isRtl = AccessibilityUtil.isLayoutRtl(getRootView());
211         if (isRtl) {
212             mCurrentLayout = ATTACHMENT_RTL_LAYOUTS_BY_COUNT[Math.min(count,
213                     ATTACHMENT_RTL_LAYOUTS_BY_COUNT.length - 1)];
214         } else {
215             mCurrentLayout = ATTACHMENT_LAYOUTS_BY_COUNT[Math.min(count,
216                     ATTACHMENT_LAYOUTS_BY_COUNT.length - 1)];
217         }
218 
219         // We must have a valid layout for the current configuration.
220         Assert.notNull(mCurrentLayout);
221 
222         mPlusNumber = count - mCurrentLayout.tiles.size();
223         Assert.isTrue(mPlusNumber >= 0);
224     }
225 
buildViews(final Iterable<MessagePartData> attachments, final ArrayList<ViewWrapper> previousViews, final Rect transitionRect)226     private void buildViews(final Iterable<MessagePartData> attachments,
227             final ArrayList<ViewWrapper> previousViews, final Rect transitionRect) {
228         final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
229         final int count = mCurrentLayout.tiles.size();
230         int i = 0;
231         final Iterator<MessagePartData> iterator = attachments.iterator();
232         while (iterator.hasNext() && i < count) {
233             final MessagePartData attachment = iterator.next();
234             ViewWrapper attachmentWrapper = null;
235             // Try to recycle a previous view first
236             for (int j = 0; j < previousViews.size(); j++) {
237                 final ViewWrapper previousView = previousViews.get(j);
238                 if (previousView.attachment.equals(attachment) &&
239                         !(previousView.attachment instanceof PendingAttachmentData)) {
240                     attachmentWrapper = previousView;
241                     previousViews.remove(j);
242                     break;
243                 }
244             }
245 
246             if (attachmentWrapper == null) {
247                 final View view = AttachmentPreviewFactory.createAttachmentPreview(layoutInflater,
248                         attachment, this, AttachmentPreviewFactory.TYPE_MULTIPLE,
249                         false /* startImageRequest */, mAttachmentClickListener);
250 
251                 if (view == null) {
252                     // createAttachmentPreview can return null if something goes wrong (e.g.
253                     // attachment has unsupported contentType)
254                     continue;
255                 }
256                 if (view instanceof AsyncImageView && mImageViewDelayLoader != null) {
257                     AsyncImageView asyncImageView = (AsyncImageView) view;
258                     asyncImageView.setDelayLoader(mImageViewDelayLoader);
259                 }
260                 addView(view);
261                 attachmentWrapper = new ViewWrapper(view, attachment);
262                 // Help animate from single to multi by copying over the prev location
263                 if (count == 2 && i == 1 && transitionRect != null) {
264                     attachmentWrapper.prevLeft = transitionRect.left;
265                     attachmentWrapper.prevTop = transitionRect.top;
266                     attachmentWrapper.prevWidth = transitionRect.width();
267                     attachmentWrapper.prevHeight = transitionRect.height();
268                 }
269             }
270             i++;
271             Assert.notNull(attachmentWrapper);
272             mPreviewViews.add(attachmentWrapper);
273 
274             // The first view will animate in using PopupTransitionAnimation, but the remaining
275             // views will slide from their previous position to their new position within the
276             // layout
277             if (i == 0) {
278                 AttachmentPreview.tryAnimateViewIn(attachment, attachmentWrapper.view);
279             }
280             attachmentWrapper.needsSlideAnimation = i > 0;
281         }
282 
283         // Build the plus text view (e.g. "+2") for when there are more attachments than what
284         // this layout can display.
285         if (mPlusNumber > 0) {
286             mPlusTextView = (TextView) layoutInflater.inflate(R.layout.attachment_more_text_view,
287                     null /* parent */);
288             mPlusTextView.setText(getResources().getString(R.string.attachment_more_items,
289                     mPlusNumber));
290             addView(mPlusTextView);
291         }
292     }
293 
294     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)295     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
296         final int maxWidth = getResources().getDimensionPixelSize(
297                 R.dimen.multiple_attachment_preview_width);
298         final int maxHeight = getResources().getDimensionPixelSize(
299                 R.dimen.multiple_attachment_preview_height);
300         final int width = Math.min(MeasureSpec.getSize(widthMeasureSpec), maxWidth);
301         final int height = maxHeight;
302         final int cellWidth = width / GRID_WIDTH;
303         final int cellHeight = height / GRID_HEIGHT;
304         final int count = mPreviewViews.size();
305         final int padding = getResources().getDimensionPixelOffset(
306                 R.dimen.multiple_attachment_preview_padding);
307         for (int i = 0; i < count; i++) {
308             final View view =  mPreviewViews.get(i).view;
309             final Tile imageTile = mCurrentLayout.tiles.get(i);
310             view.measure(imageTile.getWidthMeasureSpec(cellWidth, padding),
311                     imageTile.getHeightMeasureSpec(cellHeight, padding));
312 
313             // Now that we know the size, we can request an appropriately-sized image.
314             if (view instanceof AsyncImageView) {
315                 final ImageRequestDescriptor imageRequest =
316                         AttachmentPreviewFactory.getImageRequestDescriptorForAttachment(
317                                 mPreviewViews.get(i).attachment,
318                                 view.getMeasuredWidth(),
319                                 view.getMeasuredHeight());
320                 ((AsyncImageView) view).setImageResourceId(imageRequest);
321             }
322 
323             if (i == count - 1 && mPlusTextView != null) {
324                 // The plus text view always covers the last attachment.
325                 mPlusTextView.measure(imageTile.getWidthMeasureSpec(cellWidth, padding),
326                         imageTile.getHeightMeasureSpec(cellHeight, padding));
327             }
328         }
329         setMeasuredDimension(width, height);
330     }
331 
332     @Override
onLayout(final boolean changed, final int left, final int top, final int right, final int bottom)333     protected void onLayout(final boolean changed, final int left, final int top, final int right,
334             final int bottom) {
335         final int cellWidth = getMeasuredWidth() / GRID_WIDTH;
336         final int cellHeight = getMeasuredHeight() / GRID_HEIGHT;
337         final int padding = getResources().getDimensionPixelOffset(
338                 R.dimen.multiple_attachment_preview_padding);
339         final int count = mPreviewViews.size();
340         for (int i = 0; i < count; i++) {
341             final ViewWrapper viewWrapper =  mPreviewViews.get(i);
342             final View view = viewWrapper.view;
343             final Tile imageTile = mCurrentLayout.tiles.get(i);
344             final int tileLeft = imageTile.startX * cellWidth;
345             final int tileTop = imageTile.startY * cellHeight;
346             view.layout(tileLeft + padding, tileTop + padding,
347                     tileLeft + view.getMeasuredWidth(),
348                     tileTop + view.getMeasuredHeight());
349             if (viewWrapper.needsSlideAnimation) {
350                 trySlideAttachmentView(viewWrapper);
351                 viewWrapper.needsSlideAnimation = false;
352             } else {
353                 viewWrapper.prevLeft = view.getLeft();
354                 viewWrapper.prevTop = view.getTop();
355                 viewWrapper.prevWidth = view.getWidth();
356                 viewWrapper.prevHeight = view.getHeight();
357             }
358 
359             if (i == count - 1 && mPlusTextView != null) {
360                 // The plus text view always covers the last attachment.
361                 mPlusTextView.layout(tileLeft + padding, tileTop + padding,
362                         tileLeft + mPlusTextView.getMeasuredWidth(),
363                         tileTop + mPlusTextView.getMeasuredHeight());
364             }
365         }
366     }
367 
trySlideAttachmentView(final ViewWrapper viewWrapper)368     private void trySlideAttachmentView(final ViewWrapper viewWrapper) {
369         if (!(viewWrapper.attachment instanceof MediaPickerMessagePartData)) {
370             return;
371         }
372         final View view = viewWrapper.view;
373 
374 
375         final int xOffset = viewWrapper.prevLeft - view.getLeft();
376         final int yOffset = viewWrapper.prevTop - view.getTop();
377         final float scaleX = viewWrapper.prevWidth / (float) view.getWidth();
378         final float scaleY = viewWrapper.prevHeight / (float) view.getHeight();
379 
380         if (xOffset == 0 && yOffset == 0 && scaleX == 1 && scaleY == 1) {
381             // Layout hasn't changed
382             return;
383         }
384 
385         final AnimationSet animationSet = new AnimationSet(
386                 true /* shareInterpolator */);
387         animationSet.addAnimation(new TranslateAnimation(xOffset, 0, yOffset, 0));
388         animationSet.addAnimation(new ScaleAnimation(scaleX, 1, scaleY, 1));
389         animationSet.setDuration(
390                 UiUtils.MEDIAPICKER_TRANSITION_DURATION);
391         animationSet.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
392         view.startAnimation(animationSet);
393         view.invalidate();
394         viewWrapper.prevLeft = view.getLeft();
395         viewWrapper.prevTop = view.getTop();
396         viewWrapper.prevWidth = view.getWidth();
397         viewWrapper.prevHeight = view.getHeight();
398     }
399 
findViewForAttachment(final MessagePartData attachment)400     public View findViewForAttachment(final MessagePartData attachment) {
401         for (ViewWrapper wrapper : mPreviewViews) {
402             if (wrapper.attachment.equals(attachment) &&
403                     !(wrapper.attachment instanceof PendingAttachmentData)) {
404                 return wrapper.view;
405             }
406         }
407         return null;
408     }
409 
410     private static class ViewWrapper {
411         final View view;
412         final MessagePartData attachment;
413         boolean needsSlideAnimation;
414         int prevLeft;
415         int prevTop;
416         int prevWidth;
417         int prevHeight;
418 
ViewWrapper(final View view, final MessagePartData attachment)419         ViewWrapper(final View view, final MessagePartData attachment) {
420             this.view = view;
421             this.attachment = attachment;
422         }
423     }
424 }
425