1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
|
/*
* 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;
}
}
}
|