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 { 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