Merge "Import translations. DO NOT MERGE" into jb-ub-mail
diff --git a/res/layout/background.xml b/res/layout/background.xml
index fc067ec..84b03a5 100644
--- a/res/layout/background.xml
+++ b/res/layout/background.xml
@@ -15,15 +15,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+<View xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/background"
android:layout_width="match_parent"
android:minHeight="@dimen/conversation_item_height"
android:ellipsize="end"
android:gravity="center_vertical"
android:paddingLeft="16dip"
- android:singleLine="true"
- android:text="@string/no_conversations"
- android:textColor="@android:color/white"
- android:background="@color/leaveBehindBackground"
- android:textSize="16sp" />
\ No newline at end of file
+ android:background="@color/leaveBehindBackground" />
\ No newline at end of file
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
index 977f942..0bcdf25 100644
--- a/res/values/animation_constants.xml
+++ b/res/values/animation_constants.xml
@@ -26,6 +26,7 @@
<integer name="dialog_animationShortDur">150</integer>
<integer name="shrink_animation_duration">350</integer>
<integer name="slide_animation_duration">350</integer>
+ <integer name="fade_in_animation_duration">350</integer>
<!-- Swipe constants -->
<integer name="swipe_escape_velocity">100</integer>
diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java
index 310a67e..5e0aed5 100644
--- a/src/com/android/mail/browse/ConversationContainer.java
+++ b/src/com/android/mail/browse/ConversationContainer.java
@@ -35,6 +35,7 @@
import com.android.mail.browse.ScrollNotifier.ScrollListener;
import com.android.mail.ui.ConversationViewFragment;
import com.android.mail.utils.DequeMap;
+import com.android.mail.utils.InputSmoother;
import com.android.mail.utils.LogUtils;
import com.google.common.collect.Lists;
@@ -73,6 +74,12 @@
R.id.conversation_topmost_overlay
};
+ /**
+ * Maximum scroll speed (in dp/sec) at which the snap header animation will draw.
+ * Anything faster than that, and drawing it creates visual artifacting (wagon-wheel effect).
+ */
+ private static final float SNAP_HEADER_MAX_SCROLL_SPEED = 600f;
+
private ConversationViewAdapter mOverlayAdapter;
private int[] mOverlayBottoms;
private ConversationWebView mWebView;
@@ -158,6 +165,8 @@
private boolean mDisableLayoutTracing;
+ private final InputSmoother mVelocityTracker;
+
private final DataSetObserver mAdapterObserver = new AdapterObserver();
/**
@@ -200,6 +209,8 @@
mOverlayViews = new SparseArray<OverlayView>();
+ mVelocityTracker = new InputSmoother(c);
+
mTouchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
// Disabling event splitting fixes pinch-zoom when the first pointer goes down on the
@@ -340,6 +351,7 @@
@Override
public void onNotifierScroll(final int x, final int y) {
+ mVelocityTracker.onInput(y);
mDisableLayoutTracing = true;
positionOverlays(x, y);
mDisableLayoutTracing = false;
@@ -415,21 +427,7 @@
spacerIndex--;
}
- // render and/or re-position snap header
- ConversationOverlayItem snapItem = null;
- if (mSnapIndex != -1) {
- final ConversationOverlayItem item = mOverlayAdapter.getItem(mSnapIndex);
- if (item.canBecomeSnapHeader()) {
- snapItem = item;
- }
- }
- if (snapItem == null) {
- mSnapHeader.setVisibility(GONE);
- mSnapHeader.unbind();
- } else {
- snapItem.bindView(mSnapHeader, false /* measureOnly */);
- mSnapHeader.setVisibility(VISIBLE);
- }
+ positionSnapHeader(mSnapIndex);
}
/**
@@ -604,6 +602,9 @@
final OverlayView overlay = mOverlayViews.get(adapterIndex);
final ConversationOverlayItem item = mOverlayAdapter.getItem(adapterIndex);
+ // save off the item's current top for later snap calculations
+ item.setTop(overlayTopY);
+
// is the overlay visible and does it have non-zero height?
if (overlayTopY != overlayBottomY && overlayBottomY > mOffsetY
&& overlayTopY < mOffsetY + getHeight()) {
@@ -684,6 +685,53 @@
return view;
}
+ // render and/or re-position snap header
+ private void positionSnapHeader(int snapIndex) {
+ ConversationOverlayItem snapItem = null;
+ if (snapIndex != -1) {
+ final ConversationOverlayItem item = mOverlayAdapter.getItem(snapIndex);
+ if (item.canBecomeSnapHeader()) {
+ snapItem = item;
+ }
+ }
+ if (snapItem == null) {
+ mSnapHeader.setVisibility(GONE);
+ mSnapHeader.unbind();
+ return;
+ }
+
+ snapItem.bindView(mSnapHeader, false /* measureOnly */);
+ mSnapHeader.setVisibility(VISIBLE);
+
+ int overlap = 0;
+
+ final ConversationOverlayItem next = findNextPushingOverlay(snapIndex + 1);
+ if (next != null) {
+ overlap = Math.min(0, next.getTop() - mSnapHeader.getHeight() - mOffsetY);
+
+ // disable overlap drawing past a certain speed
+ if (overlap < 0) {
+ final Float v = mVelocityTracker.getSmoothedVelocity();
+ if (v != null && v > SNAP_HEADER_MAX_SCROLL_SPEED) {
+ overlap = 0;
+ }
+ }
+ }
+ mSnapHeader.setTranslateY(overlap);
+ }
+
+ // find the next header that can push the snap header up
+ private ConversationOverlayItem findNextPushingOverlay(int start) {
+ int value = -1;
+ for (int i = start, len = mOverlayAdapter.getCount(); i < len; i++) {
+ final ConversationOverlayItem next = mOverlayAdapter.getItem(i);
+ if (next.canPushSnapHeader()) {
+ return next;
+ }
+ }
+ return null;
+ }
+
/**
* Prevents any layouts from happening until the next time {@link #onGeometryChange(int[])} is
* called. Useful when you know the HTML spacer coordinates are inconsistent with adapter items.
diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java
index c996184..71fc772 100644
--- a/src/com/android/mail/browse/ConversationOverlayItem.java
+++ b/src/com/android/mail/browse/ConversationOverlayItem.java
@@ -29,6 +29,7 @@
public abstract class ConversationOverlayItem {
private int mHeight; // in px
+ private int mTop; // in px
private boolean mNeedsMeasure;
public static final String LOG_TAG = ConversationViewFragment.LAYOUT_TAG;
@@ -80,6 +81,14 @@
}
}
+ public int getTop() {
+ return mTop;
+ }
+
+ public void setTop(int top) {
+ mTop = top;
+ }
+
public boolean isMeasurementValid() {
return !mNeedsMeasure;
}
diff --git a/src/com/android/mail/browse/HeaderBlock.java b/src/com/android/mail/browse/HeaderBlock.java
deleted file mode 100644
index 6fb934e..0000000
--- a/src/com/android/mail/browse/HeaderBlock.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2012 Google Inc.
- * Licensed to 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.
- */
-
-package com.android.mail.browse;
-
-/**
- * A header block in the conversation view that corresponds to a region in the web content. It may
- * also be eligible for snapping.
- *
- */
-public interface HeaderBlock {
-
- /**
- * Eligible to become a a snappy header?
- */
- boolean canSnap();
- /**
- * If eligible for snapping, returns the populated header view to snap.
- */
- MessageHeaderView getSnapView();
- /**
- * Spaces out this view in its container by this number of pixels to match its message body
- * size, if any.
- */
- void setMarginBottom(int height);
- void setVisibility(int vis);
- /**
- * Called on a header when new contact info is known for the conversation. If the header
- * displays contact info, it should refresh it.
- */
- void updateContactInfo();
- /**
- * If this header can be starred/unstarred, change whether the message header appears to be
- * starred or not. Does not actually mark the backing message starred/unstarred.
- */
- void setStarDisplay(boolean starred);
-
-}
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 18ae53f..58e7cd7 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -62,7 +62,7 @@
import java.util.Map;
public class MessageHeaderView extends LinearLayout implements OnClickListener,
- OnMenuItemClickListener, HeaderBlock, ConversationContainer.DetachListener {
+ OnMenuItemClickListener, ConversationContainer.DetachListener {
/**
* Cap very long recipient lists during summary construction for efficiency.
@@ -277,16 +277,6 @@
return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded();
}
- @Override
- public boolean canSnap() {
- return isExpanded();
- }
-
- @Override
- public MessageHeaderView getSnapView() {
- return this;
- }
-
public void setSnappy(boolean snappy) {
mIsSnappy = snappy;
hideMessageDetails();
@@ -297,16 +287,6 @@
}
}
- /**
- * Check if this header's displayed data matches that of another header.
- *
- * @param other another header
- * @return true if the headers are displaying data for the same message
- */
- public boolean matches(MessageHeaderView other) {
- return other != null && mMessage != null && mMessage.equals(other.mMessage);
- }
-
@Override
public void onDetachedFromParent() {
unbind();
@@ -328,24 +308,6 @@
}
}
- public void renderUpperHeaderFrom(MessageHeaderView other) {
- mMessageHeaderItem = other.mMessageHeaderItem;
- mMessage = other.mMessage;
- mSender = other.mSender;
- mDefaultReplyAll = other.mDefaultReplyAll;
-
- mSenderNameView.setText(other.mSenderNameView.getText());
- mSenderEmailView.setText(other.mSenderEmailView.getText());
- mStarView.setSelected(other.mStarView.isSelected());
- mStarView.setContentDescription(getResources().getString(
- mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
-
- updateContactInfo();
-
- mIsDraft = other.mIsDraft;
- updateChildVisibility();
- }
-
public void initialize(FormattedDateBuilder dateBuilder, Account account,
Map<String, Address> addressCache) {
mDateBuilder = dateBuilder;
@@ -662,23 +624,6 @@
findViewById(rowRes).setVisibility(VISIBLE);
}
- @Override
- public void setMarginBottom(int bottomMargin) {
- MarginLayoutParams p = (MarginLayoutParams) getLayoutParams();
- if (p.bottomMargin != bottomMargin) {
- p.bottomMargin = bottomMargin;
- setLayoutParams(p);
- }
- }
-
- public void setMarginTop(int topMargin) {
- MarginLayoutParams p = (MarginLayoutParams) getLayoutParams();
- if (p.topMargin != topMargin) {
- p.topMargin = topMargin;
- setLayoutParams(p);
- }
- }
-
public void setTranslateY(int offsetY) {
if (mDrawTranslateY != offsetY) {
mDrawTranslateY = offsetY;
@@ -773,8 +718,7 @@
return builder.build();
}
- @Override
- public void updateContactInfo() {
+ private void updateContactInfo() {
mPresenceView.setImageDrawable(null);
mPresenceView.setVisibility(GONE);
@@ -971,13 +915,6 @@
setMessageDetailsVisibility(GONE);
}
- @Override
- public void setStarDisplay(boolean starred) {
- if (mStarView.isSelected() != starred) {
- mStarView.setSelected(starred);
- }
- }
-
private void hideCollapsedDetails() {
if (mCollapsedDetailsView != null) {
mCollapsedDetailsView.setVisibility(GONE);
diff --git a/src/com/android/mail/browse/SuperCollapsedBlock.java b/src/com/android/mail/browse/SuperCollapsedBlock.java
index 2abec7a..e97cea8 100644
--- a/src/com/android/mail/browse/SuperCollapsedBlock.java
+++ b/src/com/android/mail/browse/SuperCollapsedBlock.java
@@ -29,15 +29,13 @@
import com.android.mail.R;
import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
import com.android.mail.utils.LogTag;
-import com.android.mail.utils.LogUtils;
/**
* A header block that expands to a list of collapsed message headers. Will notify a listener on tap
* so the listener can hide the block and reveal the corresponding collapsed message headers.
*
*/
-public class SuperCollapsedBlock extends FrameLayout implements View.OnClickListener,
- HeaderBlock {
+public class SuperCollapsedBlock extends FrameLayout implements View.OnClickListener {
public interface OnClickListener {
/**
@@ -118,34 +116,4 @@
+ r.getDimensionPixelOffset(R.dimen.message_header_vertical_margin);
}
- @Override
- public boolean canSnap() {
- return false;
- }
-
- @Override
- public MessageHeaderView getSnapView() {
- return null;
- }
-
- @Override
- public void setMarginBottom(int height) {
- // no-op. should never have a matching body.
-
- // sanity check
- if (height != 0) {
- LogUtils.d(LOG_TAG, "super-collapsed block yielded unexpected body height: %d", height);
- }
- }
-
- @Override
- public void updateContactInfo() {
- // no-op
- }
-
- @Override
- public void setStarDisplay(boolean starred) {
- // no-op
- }
-
}
diff --git a/src/com/android/mail/browse/SwipeableConversationItemView.java b/src/com/android/mail/browse/SwipeableConversationItemView.java
index 6e736e7..a8c8625 100644
--- a/src/com/android/mail/browse/SwipeableConversationItemView.java
+++ b/src/com/android/mail/browse/SwipeableConversationItemView.java
@@ -23,7 +23,6 @@
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ListView;
-import android.widget.TextView;
import com.android.mail.R;
import com.android.mail.providers.Conversation;
@@ -36,7 +35,7 @@
public class SwipeableConversationItemView extends FrameLayout {
private ConversationItemView mConversationItemView;
- private TextView mBackground;
+ private View mBackground;
public SwipeableConversationItemView(Context context, String account) {
super(context);
@@ -44,14 +43,12 @@
addView(mConversationItemView);
}
- public void addBackground(Context context, String text) {
- mBackground = (TextView) findViewById(R.id.background);
+ public void addBackground(Context context) {
+ mBackground = findViewById(R.id.background);
if (mBackground == null) {
- mBackground = (TextView) LayoutInflater.from(context).inflate(R.layout.background,
- null, true);
+ mBackground = LayoutInflater.from(context).inflate(R.layout.background, null, true);
addView(mBackground, 0);
}
- mBackground.setText(text);
}
public void setBackgroundVisibility(int visibility) {
@@ -87,10 +84,9 @@
priorityArrowsEnabled, animatedAdapter);
}
- public void startUndoAnimation(String actionText, ViewMode viewMode, AnimatedAdapter listener,
- boolean swipe) {
+ public void startUndoAnimation(ViewMode viewMode, AnimatedAdapter listener, boolean swipe) {
if (swipe) {
- addBackground(getContext(), actionText);
+ addBackground(getContext());
setBackgroundVisibility(View.VISIBLE);
mConversationItemView.startSwipeUndoAnimation(viewMode, listener);
} else {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index fb16677..8d46ac9 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -297,7 +297,9 @@
if (hasLeaveBehinds()) {
Conversation conv = new Conversation((ConversationCursor) getItem(position));
if(isPositionLeaveBehind(conv)) {
- return getLeaveBehindItem(conv);
+ LeaveBehindItem fadeIn = getLeaveBehindItem(conv);
+ fadeIn.startFadeInAnimation();
+ return fadeIn;
}
}
if (convertView != null && !(convertView instanceof SwipeableConversationItemView)) {
@@ -396,8 +398,7 @@
// The undo animation consists of fading in the conversation that
// had been destroyed.
undoView = newConversationItemView(position, parent, conversation);
- undoView.startUndoAnimation(mListView.getSwipeActionText(), mActivity.getViewMode(),
- this, swipe);
+ undoView.startUndoAnimation(mActivity.getViewMode(), this, swipe);
}
return undoView;
}
diff --git a/src/com/android/mail/ui/LeaveBehindItem.java b/src/com/android/mail/ui/LeaveBehindItem.java
index f37f200..8e29e53 100644
--- a/src/com/android/mail/ui/LeaveBehindItem.java
+++ b/src/com/android/mail/ui/LeaveBehindItem.java
@@ -45,7 +45,9 @@
private Account mAccount;
private AnimatedAdapter mAdapter;
private ConversationCursor mConversationCursor;
+ private TextView mText;
private static int sShrinkAnimationDuration = -1;
+ private static int sFadeInAnimationDuration = -1;
public LeaveBehindItem(Context context) {
this(context, null);
@@ -60,6 +62,8 @@
if (sShrinkAnimationDuration == -1) {
sShrinkAnimationDuration = context.getResources().getInteger(
R.integer.shrink_animation_duration);
+ sFadeInAnimationDuration = context.getResources().getInteger(
+ R.integer.fade_in_animation_duration);
}
}
@@ -89,7 +93,8 @@
mAdapter = adapter;
mConversationCursor = (ConversationCursor) adapter.getCursor();
setData(target);
- ((TextView) findViewById(R.id.undo_descriptionview)).setText(Html.fromHtml(mUndoOp
+ mText = ((TextView) findViewById(R.id.undo_descriptionview));
+ mText.setText(Html.fromHtml(mUndoOp
.getSingularDescription(getContext(), folder)));
findViewById(R.id.undo_text).setOnClickListener(this);
findViewById(R.id.undo_icon).setOnClickListener(this);
@@ -170,6 +175,7 @@
private int mAnimatedHeight = -1;
private int mWidth;
private boolean mAnimating;
+ private boolean mFadingInText;
/**
* Start the animation on an animating view.
@@ -195,6 +201,19 @@
}
}
+
+ public void startFadeInAnimation() {
+ if (!mFadingInText) {
+ mFadingInText = true;
+ final float start = 0;
+ final float end = 1.0f;
+ ObjectAnimator fadeIn = ObjectAnimator.ofFloat(mText, "alpha", start, end);
+ fadeIn.setInterpolator(new DecelerateInterpolator(2.0f));
+ fadeIn.setDuration(sFadeInAnimationDuration);
+ fadeIn.start();
+ }
+ }
+
public void setData(Conversation conversation) {
mData = conversation;
}
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 16792e6..4841242 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -222,7 +222,7 @@
view = (SwipeableConversationItemView) v.getParent();
}
if (view != null) {
- view.addBackground(getContext(), getSwipeActionText());
+ view.addBackground(getContext());
view.setBackgroundVisibility(View.VISIBLE);
}
}
@@ -306,15 +306,4 @@
commitDestructiveActions();
return handled;
}
-
- /**
- * Get the text resource corresponding to the result of a swipe.
- */
- public String getSwipeActionText() {
- Resources res = getContext().getResources();
- if (mSwipeAction == R.id.remove_folder) {
- return res.getString(R.string.remove_folder, mFolder.name);
- }
- return res.getString(mSwipeAction == R.id.archive ? R.string.archive : R.string.delete);
- }
}
diff --git a/src/com/android/mail/utils/InputSmoother.java b/src/com/android/mail/utils/InputSmoother.java
new file mode 100644
index 0000000..eaf1e45
--- /dev/null
+++ b/src/com/android/mail/utils/InputSmoother.java
@@ -0,0 +1,100 @@
+package com.android.mail.utils;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import com.google.common.collect.Lists;
+
+import java.util.Deque;
+
+/**
+ * Utility class to calculate a velocity using a moving average filter of recent input positions.
+ * Intended to smooth out touch input events.
+ */
+public class InputSmoother {
+
+ /**
+ * Some devices have significant sampling noise: it could be that samples come in too late,
+ * or that the reported position doesn't quite match up with the time. Instantaneous velocity
+ * on these devices is too jittery to be useful in deciding whether to instantly snap, so smooth
+ * out the data using a moving average over this window size. A sample window size n will
+ * effectively average the velocity over n-1 points, so n=2 is the minimum valid value (no
+ * averaging at all).
+ */
+ private static final int SAMPLING_WINDOW_SIZE = 5;
+
+ /**
+ * The maximum elapsed time (in millis) between samples that we would consider "consecutive".
+ * Only consecutive samples will factor into the rolling average sample window.
+ * Any samples that are older than this maximum are continually purged from the sample window,
+ * so as to avoid skewing the average with irrelevant older values.
+ */
+ private static final long MAX_SAMPLE_INTERVAL_MS = 200;
+
+ /**
+ * Sampling window to calculate rolling average of scroll velocity.
+ */
+ private final Deque<Sample> mRecentSamples = Lists.newLinkedList();
+ private final float mDensity;
+
+ private static class Sample {
+ int pos;
+ long millis;
+ }
+
+ public InputSmoother(Context context) {
+ mDensity = context.getResources().getDisplayMetrics().density;
+ }
+
+ public void onInput(int pos) {
+ Sample sample;
+ final long nowMs = SystemClock.uptimeMillis();
+
+ final Sample last = mRecentSamples.peekLast();
+ if (last != null && nowMs - last.millis > MAX_SAMPLE_INTERVAL_MS) {
+ mRecentSamples.clear();
+ }
+
+ if (mRecentSamples.size() == SAMPLING_WINDOW_SIZE) {
+ sample = mRecentSamples.removeFirst();
+ } else {
+ sample = new Sample();
+ }
+ sample.pos = pos;
+ sample.millis = nowMs;
+
+ mRecentSamples.add(sample);
+ }
+
+ /**
+ * Calculates velocity based on recent inputs from {@link #onInput(int)}, averaged together to
+ * smooth out jitter.
+ *
+ * @return returns velocity in dp/s, or null if not enough samples have been collected
+ */
+ public Float getSmoothedVelocity() {
+ if (mRecentSamples.size() < 2) {
+ // need at least 2 position samples to determine a velocity
+ return null;
+ }
+
+ // calculate moving average over current window
+ int totalDistancePx = 0;
+ int prevPos = mRecentSamples.getFirst().pos;
+ final long totalTimeMs = mRecentSamples.getLast().millis - mRecentSamples.getFirst().millis;
+
+ if (totalTimeMs <= 0) {
+ // samples are really fast or bad. no answer.
+ return null;
+ }
+
+ for (Sample s : mRecentSamples) {
+ totalDistancePx += Math.abs(s.pos - prevPos);
+ prevPos = s.pos;
+ }
+ final float distanceDp = totalDistancePx / mDensity;
+ // velocity in dp per second
+ return distanceDp * 1000 / totalTimeMs;
+ }
+
+}