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
17package com.android.messaging.ui;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.util.AttributeSet;
22import android.view.LayoutInflater;
23import android.view.View;
24import android.view.animation.AnimationSet;
25import android.view.animation.ScaleAnimation;
26import android.view.animation.TranslateAnimation;
27import android.widget.FrameLayout;
28import android.widget.TextView;
29
30import com.android.messaging.R;
31import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
32import com.android.messaging.datamodel.data.MessagePartData;
33import com.android.messaging.datamodel.data.PendingAttachmentData;
34import com.android.messaging.datamodel.media.ImageRequestDescriptor;
35import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
36import com.android.messaging.util.AccessibilityUtil;
37import com.android.messaging.util.Assert;
38import com.android.messaging.util.UiUtils;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Iterator;
43import 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 */
68public class MultiAttachmentLayout extends FrameLayout {
69
70    public interface OnAttachmentClickListener {
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
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
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
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
104        public static Tile large(final int startX, final int startY) {
105            return new Tile(startX, startY, startX + 1, startY + 1);
106        }
107
108        public static Tile wide(final int startX, final int startY) {
109            return new Tile(startX, startY, startX + 1, startY);
110        }
111
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;
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
158    public MultiAttachmentLayout(final Context context, final AttributeSet attrs) {
159        super(context, attrs);
160        mPreviewViews = new ArrayList<ViewWrapper>();
161    }
162
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
180    public OnAttachmentClickListener getOnAttachmentClickListener() {
181        return mAttachmentClickListener;
182    }
183
184    public void setOnAttachmentClickListener(final OnAttachmentClickListener listener) {
185        mAttachmentClickListener = listener;
186    }
187
188    public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) {
189        mImageViewDelayLoader = delayLoader;
190    }
191
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
200    public void clearColorFilter() {
201        for (ViewWrapper viewWrapper : mPreviewViews) {
202            if (viewWrapper.view instanceof AsyncImageView) {
203                ((AsyncImageView) viewWrapper.view).clearColorFilter();
204            }
205        }
206    }
207
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
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
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
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
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
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
419        ViewWrapper(final View view, final MessagePartData attachment) {
420            this.view = view;
421            this.attachment = attachment;
422        }
423    }
424}
425