Home | History | Annotate | Download | only in ui
      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