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