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