diff options
author | Mark Wei <markwei@google.com> | 2013-07-09 16:00:00 -0700 |
---|---|---|
committer | Mark Wei <markwei@google.com> | 2013-07-12 14:00:40 -0700 |
commit | 81aea35d45b3d0191ec595562a2fcf67009845d5 (patch) | |
tree | ae690fd5a9a8aee3e2a842fe53e08f67433701aa | |
parent | a2fb3206037770eb4797c3b059d52bd862b40e44 (diff) | |
download | android_packages_apps_UnifiedEmail-81aea35d45b3d0191ec595562a2fcf67009845d5.tar.gz android_packages_apps_UnifiedEmail-81aea35d45b3d0191ec595562a2fcf67009845d5.tar.bz2 android_packages_apps_UnifiedEmail-81aea35d45b3d0191ec595562a2fcf67009845d5.zip |
Final UI for Attachment Previews.
Placeholder pulsates for images not yet loaded.
One images loads at a time, that one image will display a spinning progressbar
after a delay.
Progress bar fixes to ensure it is rotates smoothly.
Clear section before drawing to it so transparent images look right.
Avoid PhotoManager load loop.
Bug: 9745486
Bug: 9816053
Change-Id: I2e65b3e3484d6da47d4e2523404dc745b99dd04c
20 files changed, 361 insertions, 83 deletions
diff --git a/res/drawable-hdpi/ic_attachment_load.png b/res/drawable-hdpi/ic_attachment_load.png Binary files differnew file mode 100644 index 000000000..7b05b485d --- /dev/null +++ b/res/drawable-hdpi/ic_attachment_load.png diff --git a/res/drawable-hdpi/ic_spinner_inner_holo.png b/res/drawable-hdpi/ic_spinner_inner_holo.png Binary files differnew file mode 100644 index 000000000..e461e745e --- /dev/null +++ b/res/drawable-hdpi/ic_spinner_inner_holo.png diff --git a/res/drawable-hdpi/ic_spinner_outer_holo.png b/res/drawable-hdpi/ic_spinner_outer_holo.png Binary files differnew file mode 100644 index 000000000..d495aa804 --- /dev/null +++ b/res/drawable-hdpi/ic_spinner_outer_holo.png diff --git a/res/drawable-mdpi/ic_attachment_load.png b/res/drawable-mdpi/ic_attachment_load.png Binary files differnew file mode 100644 index 000000000..752149dde --- /dev/null +++ b/res/drawable-mdpi/ic_attachment_load.png diff --git a/res/drawable-mdpi/ic_spinner_inner_holo.png b/res/drawable-mdpi/ic_spinner_inner_holo.png Binary files differnew file mode 100644 index 000000000..0c0698146 --- /dev/null +++ b/res/drawable-mdpi/ic_spinner_inner_holo.png diff --git a/res/drawable-mdpi/ic_spinner_outer_holo.png b/res/drawable-mdpi/ic_spinner_outer_holo.png Binary files differnew file mode 100644 index 000000000..46061dbb3 --- /dev/null +++ b/res/drawable-mdpi/ic_spinner_outer_holo.png diff --git a/res/drawable-xhdpi/ic_attachment_load.png b/res/drawable-xhdpi/ic_attachment_load.png Binary files differnew file mode 100644 index 000000000..92ed99b50 --- /dev/null +++ b/res/drawable-xhdpi/ic_attachment_load.png diff --git a/res/drawable-xhdpi/ic_spinner_inner_holo.png b/res/drawable-xhdpi/ic_spinner_inner_holo.png Binary files differnew file mode 100644 index 000000000..273e8e8b2 --- /dev/null +++ b/res/drawable-xhdpi/ic_spinner_inner_holo.png diff --git a/res/drawable-xhdpi/ic_spinner_outer_holo.png b/res/drawable-xhdpi/ic_spinner_outer_holo.png Binary files differnew file mode 100644 index 000000000..568e9eb7b --- /dev/null +++ b/res/drawable-xhdpi/ic_spinner_outer_holo.png diff --git a/res/drawable/progress_holo.xml b/res/drawable/progress_holo.xml new file mode 100644 index 000000000..d4ad73330 --- /dev/null +++ b/res/drawable/progress_holo.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2013, 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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <rotate + android:drawable="@drawable/ic_spinner_outer_holo" + android:pivotX="50%" + android:pivotY="50%" + android:fromDegrees="0" + android:toDegrees="1080" /> + </item> + <item> + <rotate + android:drawable="@drawable/ic_spinner_inner_holo" + android:pivotX="50%" + android:pivotY="50%" + android:fromDegrees="720" + android:toDegrees="0" /> + </item> +</layer-list>
\ No newline at end of file diff --git a/res/layout/conversation_attachment_previews.xml b/res/layout/conversation_attachment_previews.xml index eb7ce2591..7887e49b3 100644 --- a/res/layout/conversation_attachment_previews.xml +++ b/res/layout/conversation_attachment_previews.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> +<!-- The height is set programmatically set in CIVC --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/attachment_previews" android:layout_width="match_parent" @@ -8,7 +9,7 @@ android:layout_marginLeft="@dimen/attachment_preview_margin_side" android:layout_marginRight="@dimen/attachment_preview_margin_side" android:visibility="gone" > - <!--todo:markwei get font color, typeface, and size from channah--> + <!-- Use dips for textSize since we want the badge to be a fixed size. --> <TextView android:id="@+id/ap_overflow" android:layout_width="@dimen/ap_overflow_count_diameter" @@ -18,12 +19,17 @@ android:layout_gravity="bottom|right" android:includeFontPadding="false" android:textStyle="bold" - android:textSize="12sp"/> - <!--todo:markwei get actual spinner asset from channah--> + android:textSize="10dp"/> + <ImageView + android:id="@+id/ap_placeholder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:src="@drawable/ic_attachment_load" /> <ImageView android:id="@+id/ap_progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:src="@drawable/spinner_holo" /> + android:src="@drawable/ic_spinner_inner_holo" /> </FrameLayout>
\ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index 5af4a3d84..3f6ccffb0 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -32,9 +32,8 @@ <color name="date_text_color">@color/dark_gray_text_color</color> <color name="message_info_text_color">@color/gray_text_color</color> <color name="subject_text_color">#333333</color> - <!--todo:markwei get overflow badge and count color from channah--> - <color name="ap_overflow_badge_color">#aaeeeeee</color> - <color name="ap_overflow_text_color">@android:color/tertiary_text_light</color> + <color name="ap_overflow_badge_color">#eeeeeeee</color> + <color name="ap_overflow_text_color">#ff4f4c4c</color> <!-- a 'checked' item is in the conversation selection set. also the 'pressed' color. --> <!-- this is holo_blue_light @ 20% opacity --> <color name="checked_item_background_color">#cfe9f3</color> diff --git a/res/values/constants.xml b/res/values/constants.xml index 517ae8dd3..4cf939934 100644 --- a/res/values/constants.xml +++ b/res/values/constants.xml @@ -115,6 +115,12 @@ <!-- Number of menu items to hide from the ActionBar by subtracting from actionbar_max_items in non-cab mode --> <integer name="actionbar_hidden_non_cab_items_no_physical_button">1</integer> + <!-- Duration of progress bar animation for attachment previews --> + <integer name="ap_progress_animation_duration">4000</integer> + <!-- Duration of placeholder pulse animation for attachment previews --> + <integer name="ap_placeholder_animation_duration">2000</integer> + <!-- Delay before showing progress bar animations for attachment previews that are loading --> + <integer name="ap_progress_animation_delay">2000</integer> <!-- Max overflow count to show for attachment previews --> <integer name="ap_overflow_max_count">99</integer> </resources> diff --git a/res/values/dimen.xml b/res/values/dimen.xml index 22fca7502..d4e337212 100644 --- a/res/values/dimen.xml +++ b/res/values/dimen.xml @@ -127,9 +127,7 @@ <dimen name="attachment_preview_margin_bottom">4dp</dimen> <dimen name="attachment_preview_margin_bottom_wide">8dp</dimen> - <!--todo:markwei get the diameter from channah--> <dimen name="ap_overflow_count_diameter">20dp</dimen> - <!--todo:markwei channah wanted 8dp but only 4dp fits read previews--> <dimen name="ap_margin_side">4dp</dimen> <dimen name="folder_minimum_width">48dip</dimen> diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java index a56a95b8b..c6da7abc0 100644 --- a/src/com/android/mail/browse/ConversationItemView.java +++ b/src/com/android/mail/browse/ConversationItemView.java @@ -36,6 +36,7 @@ import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.SystemClock; import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableString; @@ -57,8 +58,11 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; +import android.widget.AbsListView.OnScrollListener; import android.widget.TextView; import com.android.mail.R; @@ -75,6 +79,7 @@ import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreview import com.android.mail.photomanager.ContactPhotoManager; import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier; import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewIdentifier; +import com.android.mail.photomanager.PhotoManager; import com.android.mail.photomanager.PhotoManager.PhotoIdentifier; import com.android.mail.providers.Address; import com.android.mail.providers.Attachment; @@ -135,7 +140,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog private static Bitmap STATE_CALENDAR_INVITE; private static Bitmap VISIBLE_CONVERSATION_CARET; private static Drawable RIGHT_EDGE_TABLET; - private static Bitmap PROGRESS_BAR; + private static Bitmap PLACEHOLDER; + private static Drawable PROGRESS_BAR; private static String sSendersSplitToken; private static String sElidedPaddingToken; @@ -155,8 +161,10 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog private static int sShrinkAnimationDuration; private static int sSlideAnimationDuration; private static int sAnimatingBackgroundColor; - // todo:markwei get duration from channah private static int sProgressAnimationDuration; + private static float sPlaceholderAnimationDurationRatio; + private static int sProgressAnimationDelay; + private static Interpolator sPulseAnimationInterpolator; private static int sOverflowCountMax; // Static paints. @@ -228,8 +236,12 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog * this animator does not remove the progress bars. */ private final ObjectAnimator mProgressAnimator; + private long mProgressAnimatorCancelledTime; + /** Range from 0.0f to 1.0f. */ private float mAnimatedProgressFraction; - private boolean[] mImagesLoaded = new boolean[0]; + private int[] mImagesLoaded = new int[0]; + private boolean mShowProgressBar; + private Runnable mSetShowProgressBarRunnable; private static final boolean CONVLIST_ATTACHMENT_PREVIEWS_ENABLED = true; static { @@ -237,18 +249,24 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog sFoldersPaint.setAntiAlias(true); } - public static void setPhotoManagersPaused(boolean shouldPause) { + public static void setScrollStateChanged(final int scrollState) { if (sContactPhotoManager == null) { return; } + final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE; + final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING; - if (shouldPause) { - sContactPhotoManager.pause(); + if (scrolling) { sAttachmentPreviewsManager.pause(); } else { - sContactPhotoManager.resume(); sAttachmentPreviewsManager.resume(); } + + if (flinging) { + sContactPhotoManager.pause(); + } else { + sContactPhotoManager.resume(); + } } /** @@ -429,8 +447,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, R.drawable.ic_carrot_holo); RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet); -// todo:markwei get actual spinner asset from channah - PROGRESS_BAR = BitmapFactory.decodeResource(res, drawable.spinner_holo); + PLACEHOLDER = BitmapFactory.decodeResource(res, drawable.ic_attachment_load); + PROGRESS_BAR = res.getDrawable(drawable.progress_holo); // Initialize colors. sActivatedTextColor = res.getColor(R.color.senders_text_color_read); @@ -462,8 +480,13 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding); sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context); sAttachmentPreviewsManager = new AttachmentPreviewsManager(context); - // todo:markwei get animation duration from channah - sProgressAnimationDuration = 1000; + sProgressAnimationDuration = res.getInteger(integer.ap_progress_animation_duration); + final int placeholderAnimationDuration = res + .getInteger(integer.ap_placeholder_animation_duration); + sPlaceholderAnimationDurationRatio = sProgressAnimationDuration + / placeholderAnimationDuration; + sProgressAnimationDelay = res.getInteger(integer.ap_progress_animation_delay); + sPulseAnimationInterpolator = new AccelerateDecelerateInterpolator(); sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count); } @@ -503,6 +526,14 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog }); mProgressAnimator = createProgressAnimator(); + mSetShowProgressBarRunnable = new Runnable() { + @Override + public void run() { + LogUtils.v(LOG_TAG, "progress bar: >>> set to true"); + // It's OK to set this field to true when the status is no longer LOADING. + mShowProgressBar = true; + } + }; Utils.traceEndSection(); } @@ -518,6 +549,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog private void bind(ConversationItemViewModel header, ControllableActivity activity, ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage, boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) { + boolean attachmentPreviewsChanged = false; if (mHeader != null) { // If this was previously bound to a different conversation, remove any contact photo // manager requests. @@ -541,6 +573,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog != mHeader.conversation.attachmentPreviewsCount || !header.conversation.getAttachmentPreviewUris() .equals(mHeader.conversation.getAttachmentPreviewUris())) { + attachmentPreviewsChanged = true; ArrayList<String> divisionIds = mAttachmentPreviewsCanvas.getDivisionIds(); if (divisionIds != null) { mAttachmentPreviewsCanvas.reset(); @@ -565,7 +598,10 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog mStarEnabled = folder != null && !folder.isTrash(); mSwipeEnabled = swipeEnabled; mAdapter = adapter; - mImagesLoaded = new boolean[mHeader.conversation.getAttachmentPreviewUris().size()]; + final int attachmentPreviewsSize = mHeader.conversation.getAttachmentPreviewUris().size(); + if (attachmentPreviewsChanged || mImagesLoaded.length != attachmentPreviewsSize) { + mImagesLoaded = new int[attachmentPreviewsSize]; + } if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) { mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO; @@ -878,8 +914,6 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog Utils.traceBeginSection("Setup load attachment previews"); LogUtils.d(LOG_TAG, - "loadAttachmentPreviews: ###############################################"); - LogUtils.d(LOG_TAG, "loadAttachmentPreviews: Loading attachment previews for conversation %s", mHeader.conversation); @@ -897,7 +931,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog final String uri = attachmentUris.get(i); // Find the rendition to load based on availability. - LogUtils.d(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ", + LogUtils.v(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ", Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST), Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE), uri); @@ -927,59 +961,132 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog // Second pass: Find the dimensions to load and start the load request final ImageCanvas.Dimensions canvasDimens = new ImageCanvas.Dimensions(); for (int i = 0; i < displayCount; i++) { + Utils.traceBeginSection("finding dimensions"); final PhotoIdentifier photoIdentifier = ids.get(i); final Object key = keys.get(i); mAttachmentPreviewsCanvas.getDesiredDimensions(key, canvasDimens); - if (i < mImagesLoaded.length) { - // We want to show default progress image - mImagesLoaded[i] = false; - if (!mProgressAnimator.isStarted()) { - LogUtils.d(LOG_TAG, "progress animator: >> started"); - mProgressAnimator.setCurrentPlayTime( - (long) (sProgressAnimationDuration * mAnimatedProgressFraction)); - mProgressAnimator.start(); + Utils.traceEndSection(); + + Utils.traceBeginSection("start animator"); + // We want to show default progress image + setImageLoaded(i, PhotoManager.STATUS_NOT_LOADED); + if (!mProgressAnimator.isStarted()) { + LogUtils.v(LOG_TAG, "progress animator: >> started"); + // Reduce progress bar stutter caused by reset()/bind() being called multiple + // times. + final long time = SystemClock.uptimeMillis(); + final long dt = time - mProgressAnimatorCancelledTime; + float passedFraction = 0; + if (mProgressAnimatorCancelledTime != 0 && dt > 0) { + mProgressAnimatorCancelledTime = 0; + passedFraction = (float) dt / sProgressAnimationDuration % 1.0f; + LogUtils.v(LOG_TAG, "progress animator: correction for dt %d, fraction %f", + dt, passedFraction); } + mProgressAnimator.start(); + // Wow.. this must be called after start(). + mProgressAnimator.setCurrentPlayTime((long) (sProgressAnimationDuration * ( + (mAnimatedProgressFraction + passedFraction) % 1.0f))); } + Utils.traceEndSection(); + + Utils.traceBeginSection("start load"); LogUtils.d(LOG_TAG, "loadAttachmentPreviews: start loading %s", photoIdentifier); sAttachmentPreviewsManager .loadThumbnail(photoIdentifier, mAttachmentPreviewsCanvas, canvasDimens, this); + Utils.traceEndSection(); } + Utils.traceEndSection(); } @Override - public void onImageDrawn(Object key, boolean success) { - Utils.traceBeginSection("on image drawn"); - String uri = AttachmentPreviewsManager.transformKeyToUri(key); - int index = mHeader.conversation.getAttachmentPreviewUris().indexOf(uri); - - if (index < 0 || index >= mImagesLoaded.length) { - Utils.traceEndSection(); + public void onImageDrawn(final Object key, final boolean success) { + if (mHeader == null || mHeader.conversation == null) { return; } + Utils.traceBeginSection("on image drawn"); + final String uri = AttachmentPreviewsManager.transformKeyToUri(key); + final int index = mHeader.conversation.getAttachmentPreviewUris().indexOf(uri); - LogUtils.d(LOG_TAG, + LogUtils.v(LOG_TAG, "loadAttachmentPreviews: <= onImageDrawn callback [%b] on index %d for %s", success, index, key); // We want to hide the spinning progress bar when we draw something. - mImagesLoaded[index] = success; + setImageLoaded(index, + success ? PhotoManager.STATUS_LOADED : PhotoManager.STATUS_NOT_LOADED); if (mProgressAnimator.isStarted() && areAllImagesLoaded()) { - LogUtils.d(LOG_TAG, "progress animator: << stopped"); + LogUtils.v(LOG_TAG, "progress animator: << stopped"); mProgressAnimator.cancel(); } Utils.traceEndSection(); } + @Override + public void onImageLoadStarted(final Object key) { + if (mHeader == null || mHeader.conversation == null) { + return; + } + final String uri = AttachmentPreviewsManager.transformKeyToUri(key); + final int index = mHeader.conversation.getAttachmentPreviewUris().indexOf(uri); + + LogUtils.v(LOG_TAG, + "loadAttachmentPreviews: <= onImageLoadStarted callback on index %d for %s", index, + key); + setImageLoaded(index, PhotoManager.STATUS_LOADING); + } + private boolean areAllImagesLoaded() { for (int i = 0; i < mImagesLoaded.length; i++) { - if (!mImagesLoaded[i]) { + if (mImagesLoaded[i] != PhotoManager.STATUS_LOADED) { return false; } } return true; } + /** + * Update the #mImagesLoaded state array with special logic. + * @param index Which attachment preview's state to update. + * @param status What the new state is. + */ + private void setImageLoaded(final int index, final int status) { + if (index < 0 || index >= mImagesLoaded.length) { + return; + } + final int prevStatus = mImagesLoaded[index]; + switch (status) { + case PhotoManager.STATUS_NOT_LOADED: + // Cannot transition directly from LOADING to NOT_LOADED. + if (prevStatus != PhotoManager.STATUS_LOADING) { + mImagesLoaded[index] = status; + } + break; + case PhotoManager.STATUS_LOADING: + // All other statuses must be set to not loading. + for (int i = 0; i < mImagesLoaded.length; i++) { + if (i != index && mImagesLoaded[i] == PhotoManager.STATUS_LOADING) { + mImagesLoaded[i] = PhotoManager.STATUS_NOT_LOADED; + } + } + mImagesLoaded[index] = status; + + if (prevStatus != PhotoManager.STATUS_LOADING) { + // Progress bar should only be shown after a delay + LogUtils.v(LOG_TAG, "progress bar: <<< set to false"); + mShowProgressBar = false; + LogUtils.v(LOG_TAG, "progress bar: === start delay"); + removeCallbacks(mSetShowProgressBarRunnable); + postDelayed(mSetShowProgressBarRunnable, sProgressAnimationDelay); + } + break; + case PhotoManager.STATUS_LOADED: + mImagesLoaded[index] = status; + break; + } + } + private static int makeExactSpecForSize(int size) { return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } @@ -1421,8 +1528,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog // Overflow badge and count if (getOverflowCountVisible() && areAllImagesLoaded()) { - float radius = mCoordinates.overflowDiameter / 2; - // todo:markwei get color of overflow badge from channah + final float radius = mCoordinates.overflowDiameter / 2; sPaint.setColor(sOverflowBadgeColor); canvas.drawCircle(mCoordinates.overflowXEnd - radius, mCoordinates.overflowYEnd - radius, radius, sPaint); @@ -1435,12 +1541,35 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog // Progress bar if (mProgressAnimator.isRunning()) { + // Fade from 55 -> 255 -> 55. Each cycle lasts for #sProgressAnimationDuration secs. + final int maxAlpha = 255, minAlpha = 55; + final int range = maxAlpha - minAlpha; + // We want the placeholder to pulse at a different rate from the progressbar to + // spin. + final float placeholderAnimFraction = mAnimatedProgressFraction + * sPlaceholderAnimationDurationRatio; + // During the time that placeholderAnimFraction takes to go from 0 to 1, we + // want to go all the way to #maxAlpha and back down to #minAlpha. So from 0 to 0.5, + // we increase #modifiedProgress from 0 to 1, while from 0.5 to 1 we decrease + // accordingly from 1 to 0. Math. + final float modifiedProgress = -2 * Math.abs(placeholderAnimFraction - 0.5f) + 1; + // Make it feel like a heart beat. + final float interpolatedProgress = sPulseAnimationInterpolator + .getInterpolation(modifiedProgress); + // More math. + final int alpha = (int) (interpolatedProgress * range + minAlpha); + sPaint.setAlpha(alpha); + final int count = mImagesLoaded.length; for (int i = 0; i < count; i++) { - if (!mImagesLoaded[i]) { + if (mShowProgressBar && mImagesLoaded[i] == PhotoManager.STATUS_LOADING) { + // status is LOADING and enough time has passed canvas.save(); drawProgressBar(canvas, i, count); canvas.restore(); + } else if (mImagesLoaded[i] != PhotoManager.STATUS_LOADED) { + // status is either NOT_LOADED or LOADING + drawPlaceholder(canvas, i, count); } } } @@ -1484,6 +1613,21 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog } /** + * Draws the specified placeholder on the canvas. + * @param canvas The canvas to draw on. + * @param index If drawing multiple progress bars, this determines which one we are drawing. + * @param total Whether we are drawing multiple progress bars. + */ + private void drawPlaceholder(Canvas canvas, int index, int total) { + int placeholderX = getPlaceholderX(index, total); + if (placeholderX == -1) { + return; + } + + canvas.drawBitmap(PLACEHOLDER, placeholderX, mCoordinates.placeholderY, sPaint); + } + + /** * Draws the specified progress bar on the canvas. * @param canvas The canvas to draw on. * @param index If drawing multiple progress bars, this determines which one we are drawing. @@ -1495,18 +1639,19 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog return; } - // We want to rotate counter-clockwise, because that's the direction the asset faces - canvas.rotate(360 - mAnimatedProgressFraction * 360, - progressBarX + mCoordinates.progressBarWidth / 2, - mCoordinates.progressBarY + mCoordinates.progressBarHeight / 2); - - canvas.drawBitmap(PROGRESS_BAR, progressBarX, mCoordinates.progressBarY, null); + // Set the level from 0 to 10000 to animate the Drawable. + PROGRESS_BAR.setLevel((int) (mAnimatedProgressFraction * 10000)); + // canvas.translate() for Bitmaps, setBounds() for Drawables. + PROGRESS_BAR.setBounds(progressBarX, mCoordinates.progressBarY, + progressBarX + mCoordinates.progressBarWidth, + mCoordinates.progressBarY + mCoordinates.progressBarHeight); + PROGRESS_BAR.draw(canvas); } /** * @see com.android.mail.browse.ConversationItemView#drawProgressBar */ - private void invalidateProgressBar(int index, int total) { + private void invalidatePlaceholderAndProgressBar(int index, int total) { int progressBarX = getProgressBarX(index, total); if (progressBarX == -1) { return; @@ -1515,6 +1660,25 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog invalidate(progressBarX, mCoordinates.progressBarY, progressBarX + mCoordinates.progressBarWidth, mCoordinates.progressBarY + mCoordinates.progressBarHeight); + + int placeholderX = getPlaceholderX(index, total); + if (placeholderX == -1) { + return; + } + + invalidate(placeholderX, mCoordinates.placeholderY, + placeholderX + mCoordinates.placeholderWidth, + mCoordinates.placeholderY + mCoordinates.placeholderHeight); + } + + private int getPlaceholderX(int index, int total) { + if (mCoordinates == null) { + return -1; + } + int sectionWidth = mCoordinates.attachmentPreviewsWidth / total; + int sectionOffset = index * sectionWidth; + return mCoordinates.attachmentPreviewsX + sectionOffset + sectionWidth / 2 + - mCoordinates.placeholderWidth / 2; } private int getProgressBarX(int index, int total) { @@ -1800,9 +1964,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog setAlpha(1f); setTranslationX(0f); mAnimatedHeightFraction = 1.0f; - LogUtils.d(LOG_TAG, "progress animator: cancelling after %dms", sProgressAnimationDuration); if (mProgressAnimator.isStarted()) { - LogUtils.d(LOG_TAG, "progress animator: << stopped"); + LogUtils.v(LOG_TAG, "progress animator: << stopped"); mProgressAnimator.cancel(); } Utils.traceEndSection(); @@ -1899,26 +2062,28 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog } private ObjectAnimator createProgressAnimator() { - ObjectAnimator animator = ObjectAnimator.ofFloat(this, "animatedProgressFraction", 0f, 1.0f) - .setDuration(sProgressAnimationDuration); + final ObjectAnimator animator = ObjectAnimator + .ofFloat(this, "animatedProgressFraction", 0f, 1.0f).setDuration( + sProgressAnimationDuration); animator.setInterpolator(new LinearInterpolator()); animator.setRepeatCount(ObjectAnimator.INFINITE); animator.setRepeatMode(ObjectAnimator.RESTART); animator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(final Animator animation) { invalidateAll(); } @Override - public void onAnimationCancel(Animator animation) { + public void onAnimationCancel(final Animator animation) { invalidateAll(); + mProgressAnimatorCancelledTime = SystemClock.uptimeMillis(); } private void invalidateAll() { - int count = mHeader.conversation.getAttachmentPreviewUris().size(); + final int count = mHeader.conversation.getAttachmentPreviewUris().size(); for (int i = 0; i < count; i++) { - invalidateProgressBar(i, count); + invalidatePlaceholderAndProgressBar(i, count); } } }); @@ -1926,12 +2091,16 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog } // Used by animator - public void setAnimatedProgressFraction(float fraction) { + public void setAnimatedProgressFraction(final float fraction) { + // ObjectAnimator.cancel() sets the field to 0.0f. + if (fraction == 0.0f) { + return; + } mAnimatedProgressFraction = fraction; final int count = mImagesLoaded.length; for (int i = 0; i < count; i++) { - if (!mImagesLoaded[i]) { - invalidateProgressBar(i, count); + if (mImagesLoaded[i] != PhotoManager.STATUS_LOADED) { + invalidatePlaceholderAndProgressBar(i, count); } } } diff --git a/src/com/android/mail/browse/ConversationItemViewCoordinates.java b/src/com/android/mail/browse/ConversationItemViewCoordinates.java index 34b5596b3..543f63657 100644 --- a/src/com/android/mail/browse/ConversationItemViewCoordinates.java +++ b/src/com/android/mail/browse/ConversationItemViewCoordinates.java @@ -250,6 +250,10 @@ public class ConversationItemViewCoordinates { final float overflowFontSize; final Typeface overflowTypeface; + // Attachment previews placeholder + final int placeholderY; + final int placeholderWidth; + final int placeholderHeight; // Attachment previews progress bar final int progressBarY; final int progressBarWidth; @@ -483,6 +487,12 @@ public class ConversationItemViewCoordinates { overflowFontSize = overflow.getTextSize(); overflowTypeface = overflow.getTypeface(); + final View placeholder = view.findViewById(id.ap_placeholder); + placeholderWidth = placeholder.getWidth(); + placeholderHeight = placeholder.getHeight(); + placeholderY = attachmentPreviewsY + attachmentPreviewsHeight / 2 + - placeholderHeight / 2; + final View progressBar = view.findViewById(id.ap_progress_bar); progressBarWidth = progressBar.getWidth(); progressBarHeight = progressBar.getHeight(); @@ -498,6 +508,9 @@ public class ConversationItemViewCoordinates { overflowDiameter = 0; overflowFontSize = 0; overflowTypeface = null; + placeholderY = 0; + placeholderWidth = 0; + placeholderHeight = 0; progressBarY = 0; progressBarWidth = 0; progressBarHeight = 0; diff --git a/src/com/android/mail/photomanager/AttachmentPreviewsManager.java b/src/com/android/mail/photomanager/AttachmentPreviewsManager.java index dc584f34e..10149e973 100644 --- a/src/com/android/mail/photomanager/AttachmentPreviewsManager.java +++ b/src/com/android/mail/photomanager/AttachmentPreviewsManager.java @@ -85,6 +85,18 @@ public class AttachmentPreviewsManager extends PhotoManager { } @Override + protected void onImageLoadStarted(final Request request) { + if (request == null) { + return; + } + final Object key = request.getKey(); + if (mCallbacks.containsKey(key)) { + AttachmentPreviewsManagerCallback callback = mCallbacks.get(key); + callback.onImageLoadStarted(request.getKey()); + } + } + + @Override protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) { float ratio = (float) newWidth / prevWidth; boolean previousRequestSmaller = newWidth > prevWidth @@ -235,7 +247,7 @@ public class AttachmentPreviewsManager extends PhotoManager { } Attachment attachment = null; try { - LogUtils.d(TAG, "AttachmentPreviewsManager: found %d attachments for uri %s", + LogUtils.v(TAG, "AttachmentPreviewsManager: found %d attachments for uri %s", cursor.getCount(), uri); if (cursor.moveToFirst()) { attachment = new Attachment(cursor); @@ -245,7 +257,7 @@ public class AttachmentPreviewsManager extends PhotoManager { } if (attachment == null) { - LogUtils.d(TAG, "AttachmentPreviewsManager: attachment not found for uri %s", + LogUtils.w(TAG, "AttachmentPreviewsManager: attachment not found for uri %s", uri); Utils.traceEndSection(); continue; @@ -258,14 +270,14 @@ public class AttachmentPreviewsManager extends PhotoManager { } else if (id.rendition == AttachmentRendition.SIMPLE) { contentUri = attachment.thumbnailUri; } else { - LogUtils.d(TAG, + LogUtils.w(TAG, "AttachmentPreviewsManager: Cannot load rendition %d for uri %s", id.rendition, uri); Utils.traceEndSection(); continue; } - LogUtils.d(TAG, "AttachmentPreviewsManager: attachments has contentUri %s", + LogUtils.v(TAG, "AttachmentPreviewsManager: attachments has contentUri %s", contentUri); final InputStreamFactory factory = new InputStreamFactory() { @Override @@ -348,5 +360,7 @@ public class AttachmentPreviewsManager extends PhotoManager { public interface AttachmentPreviewsManagerCallback { public void onImageDrawn(Object key, boolean success); + + public void onImageLoadStarted(Object key); } } diff --git a/src/com/android/mail/photomanager/PhotoManager.java b/src/com/android/mail/photomanager/PhotoManager.java index 57d2fa136..66c1058b2 100644 --- a/src/com/android/mail/photomanager/PhotoManager.java +++ b/src/com/android/mail/photomanager/PhotoManager.java @@ -47,6 +47,9 @@ import java.util.concurrent.atomic.AtomicInteger; * Asynchronously loads photos and maintains a cache of photos */ public abstract class PhotoManager implements ComponentCallbacks2, Callback { + public static final int STATUS_NOT_LOADED = 0; + public static final int STATUS_LOADING = 1; + public static final int STATUS_LOADED = 2; /** * Get the default image provider that draws while the photo is being * loaded. @@ -64,12 +67,20 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver); /** - * Subclasses can implement this method to alert callbacks of the images' loading progress. + * Subclasses can implement this method to alert callbacks that images finished loading. * @param request The original request made. * @param success True if we successfully loaded the image from cache. False if we fell back * to the default image. */ - protected void onImageDrawn(Request request, boolean success) { + protected void onImageDrawn(final Request request, final boolean success) { + // Subclasses can choose to do something about this + } + + /** + * Subclasses can implement this method to alert callbacks that images started loading. + * @param request The original request made. + */ + protected void onImageLoadStarted(final Request request) { // Subclasses can choose to do something about this } @@ -108,6 +119,11 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { */ private static final int MESSAGE_PHOTOS_LOADED = 2; + /** + * Type of message sent by the loader thread to indicate that + */ + private static final int MESSAGE_PHOTO_LOADING = 3; + public interface DefaultImageProvider { /** * Applies the default avatar to the DividedImageView. Extent is an @@ -154,6 +170,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { } } + // todo:ath caches should be member vars /** * An LRU cache for bitmap holders. The cache contains bytes for photos just * as they come from the database. Each holder has a soft reference to the @@ -326,13 +343,14 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { /** * Checks if the photo is present in cache. If so, sets the photo on the view. * - * @param request Determines which image to load from cache. + * @param request Determines which image to load from cache. * @param afterLoaderThreadFinished Pass true if calling after the LoaderThread has run. Pass * false if the Loader Thread hasn't made any attempts to * load images yet. * @return false if the photo needs to be (re)loaded from the provider. */ - private boolean loadCachedPhoto(Request request, boolean afterLoaderThreadFinished) { + private boolean loadCachedPhoto(final Request request, + final boolean afterLoaderThreadFinished) { Utils.traceBeginSection("Load cached photo"); final Bitmap cached = getCachedPhoto(request.bitmapKey); if (cached != null) { @@ -345,8 +363,8 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { Thread.currentThread()); } if (request.getView().getGeneration() == request.viewGeneration) { - onImageDrawn(request, true); request.getView().drawImage(cached, request.getKey()); + onImageDrawn(request, true); } Utils.traceEndSection(); return true; @@ -369,8 +387,8 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { Thread.currentThread()); } if (request.getView().getGeneration() == request.viewGeneration) { - onImageDrawn(request, true); request.getView().drawImage(cachedReplacement, request.getKey()); + onImageDrawn(request, true); } Utils.traceEndSection(); return false; @@ -405,6 +423,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { * Temporarily stops loading photos from the database. */ public void pause() { + LogUtils.d(TAG, "%s paused.", getClass().getName()); mPaused = true; } @@ -412,6 +431,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { * Resumes loading photos from the database. */ public void resume() { + LogUtils.d(TAG, "%s resumed.", getClass().getName()); mPaused = false; if (DEBUG) dumpStats(); if (!mPendingRequests.isEmpty()) { @@ -436,7 +456,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { * Processes requests on the main thread. */ @Override - public boolean handleMessage(Message msg) { + public boolean handleMessage(final Message msg) { switch (msg.what) { case MESSAGE_REQUEST_LOADING: { mLoadingRequested = false; @@ -454,6 +474,15 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { if (DEBUG) dumpStats(); return true; } + + case MESSAGE_PHOTO_LOADING: { + if (!mPaused) { + final int hashcode = msg.arg1; + final Request request = mPendingRequests.get(hashcode); + onImageLoadStarted(request); + } + return true; + } } return false; } @@ -468,7 +497,10 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { for (Integer hash : mPendingRequests.keySet()) { Request request = mPendingRequests.get(hash); boolean loaded = loadCachedPhoto(request, true); - if (loaded) { + // Request can go through multiple attempts if the LoaderThread fails to load any + // images for it, or if the images it loads are evicted from the cache before we + // could access them in the main thread. + if (loaded || request.attempts > 2) { toRemove.add(hash); } } @@ -476,9 +508,6 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { mPendingRequests.remove(key); } - // TODO: this already seems to happen when calling loadCachedPhoto - //softenCache(); - if (!mPendingRequests.isEmpty()) { LogUtils.d(TAG, "Finished loading batch. %d still have to be loaded.", mPendingRequests.size()); @@ -695,6 +724,11 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { loadRequests.add(request); decodeRequests.add(request); batchCount++; + + final Message msg = Message.obtain(); + msg.what = MESSAGE_PHOTO_LOADING; + msg.arg1 = request.hashCode(); + mMainThreadHandler.sendMessage(msg); } else { // Even if the image load is already done, this particular decode configuration // may not yet have run. Be sure to add it to the queue. @@ -702,6 +736,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { decodeRequests.add(request); } } + request.attempts++; if (maxBatchCount > 0 && batchCount >= maxBatchCount) { break; } @@ -869,6 +904,7 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { private final ImageCanvas mView; public final BitmapIdentifier bitmapKey; public final int viewGeneration; + public int attempts; private Request(PhotoIdentifier photoIdentifier, DefaultImageProvider defaultProvider, ImageCanvas view, ImageCanvas.Dimensions dimensions) { @@ -953,6 +989,10 @@ public abstract class PhotoManager implements ComponentCallbacks2, Callback { @Override public int compareTo(Request another) { + // Hold off on loading Requests which have failed before so it don't hold up others + if (attempts - another.attempts != 0) { + return attempts - another.attempts; + } return mPhotoIdentifier.compareTo(another.mPhotoIdentifier); } } diff --git a/src/com/android/mail/ui/DividedImageCanvas.java b/src/com/android/mail/ui/DividedImageCanvas.java index e40ff12cf..2880ea4d3 100644 --- a/src/com/android/mail/ui/DividedImageCanvas.java +++ b/src/com/android/mail/ui/DividedImageCanvas.java @@ -157,6 +157,7 @@ public class DividedImageCanvas implements ImageCanvas { // l t r b sSrc.set(0, srcTop, b.getWidth(), srcBottom); sDest.set(left, top, right, bottom); + mCanvas.drawRect(sDest, sClearPaint); mCanvas.drawBitmap(b, sSrc, sDest, sPaint); } else { // clear @@ -372,8 +373,6 @@ public class DividedImageCanvas implements ImageCanvas { * Draw the contents of the DividedImageCanvas to the supplied canvas. */ public void draw(Canvas canvas) { - // todo:markwei we can see the old image behind transparency regions. Should we also - // "clear" the canvas? ath if (mDividedBitmap != null && mBitmapValid) { canvas.drawBitmap(mDividedBitmap, 0, 0, null); } diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java index 80cf6defc..24189fd7f 100644 --- a/src/com/android/mail/ui/SwipeableListView.java +++ b/src/com/android/mail/ui/SwipeableListView.java @@ -373,7 +373,7 @@ public class SwipeableListView extends ListView implements Callback, OnScrollLis } @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { + public void onScrollStateChanged(final AbsListView view, final int scrollState) { mScrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE; if (!mScrolling) { @@ -387,7 +387,7 @@ public class SwipeableListView extends ListView implements Callback, OnScrollLis } if (SCROLL_PAUSE_ENABLE) { - ConversationItemView.setPhotoManagersPaused(mScrolling); + ConversationItemView.setScrollStateChanged(scrollState); } } |