From 763ff2752d1b280f2561fe9e9cfcd046a84a17a2 Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Wed, 2 Nov 2011 16:39:23 -0700 Subject: [PATCH 1/2] Scroll to the last known position in the MessageView when rotating the screen. --- src/com/fsck/k9/activity/MessageView.java | 40 +++++++++++++++- .../fsck/k9/controller/MessagingListener.java | 5 ++ src/com/fsck/k9/view/AccessibleWebView.java | 15 ++++++ src/com/fsck/k9/view/MessageWebView.java | 22 +++++++++ src/com/fsck/k9/view/SingleMessageView.java | 21 ++++++++- src/com/fsck/k9/view/ToggleScrollView.java | 46 +++++++++++++++++++ 6 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index d10ee7186..58efce0ef 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -38,6 +38,7 @@ public class MessageView extends K9Activity implements OnClickListener { private static final String EXTRA_MESSAGE_REFERENCES = "com.fsck.k9.MessageView_messageReferences"; private static final String EXTRA_ORIGINATING_INTENT = "com.fsck.k9.MessageView_originatingIntent"; private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next"; + private static final String EXTRA_SCROLL_PERCENTAGE = "com.fsck.k9.MessageView_scrollPercentage"; private static final String SHOW_PICTURES = "showPictures"; private static final String STATE_PGP_DATA = "pgpData"; private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; @@ -96,6 +97,7 @@ public class MessageView extends K9Activity implements OnClickListener { private Listener mListener = new Listener(); private MessageViewHandler mHandler = new MessageViewHandler(); private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); + private MessagingListener mLoadCompleteListener = new ScrollToLastLocationListener(); /** this variable is used to save the calling AttachmentView * until the onActivityResult is called. @@ -109,6 +111,11 @@ public class MessageView extends K9Activity implements OnClickListener { */ private String mDstFolder; + /** + * Used after restore/rotation to scroll our message to the last known location. + */ + private double mScrollPercentage; + private final class StorageListenerImplementation implements StorageManager.StorageListener { @Override public void onUnmount(String providerId) { @@ -408,8 +415,14 @@ public class MessageView extends K9Activity implements OnClickListener { } }; }); + mMessageView.initialize(this); + // Add listener for message load completion. We'll use this to scroll the page to user's last known + // location. + mController.addListener(mLoadCompleteListener); + mMessageView.setListeners(mController.getListeners()); + setTitle(""); final Intent intent = getIntent(); @@ -530,6 +543,9 @@ public class MessageView extends K9Activity implements OnClickListener { outState.putParcelableArrayList(EXTRA_MESSAGE_REFERENCES, mMessageReferences); outState.putSerializable(STATE_PGP_DATA, mPgpData); outState.putBoolean(SHOW_PICTURES, mMessageView.showPictures()); + if(mTopView != null) { + outState.putDouble(EXTRA_SCROLL_PERCENTAGE, mTopView.getScrollPercentage()); + } } @Override @@ -538,6 +554,7 @@ public class MessageView extends K9Activity implements OnClickListener { mPgpData = (PgpData) savedInstanceState.getSerializable(STATE_PGP_DATA); mMessageView.updateCryptoLayout(mAccount.getCryptoProvider(), mPgpData, mMessage); mMessageView.setLoadPictures(savedInstanceState.getBoolean(SHOW_PICTURES)); + mScrollPercentage = savedInstanceState.getDouble(EXTRA_SCROLL_PERCENTAGE); } private void displayMessage(MessageReference ref) { @@ -657,11 +674,13 @@ public class MessageView extends K9Activity implements OnClickListener { onAccountUnavailable(); return; } + mController.addListener(mLoadCompleteListener); StorageManager.getInstance(getApplication()).addListener(mStorageListener); } @Override protected void onPause() { + mController.removeListener(mLoadCompleteListener); StorageManager.getInstance(getApplication()).removeListener(mStorageListener); super.onPause(); } @@ -873,6 +892,8 @@ public class MessageView extends K9Activity implements OnClickListener { @Override protected void onNext() { + // Reset scroll percentage when we change messages + mScrollPercentage = 0; if (mNextMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; @@ -888,6 +909,8 @@ public class MessageView extends K9Activity implements OnClickListener { @Override protected void onPrevious() { + // Reset scroll percentage when we change messages + mScrollPercentage = 0; if (mPreviousMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; @@ -1115,8 +1138,8 @@ public class MessageView extends K9Activity implements OnClickListener { mTopView.scrollTo(0, 0); try { if (MessageView.this.mMessage != null - && MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL) - && message.isSet(Flag.X_DOWNLOADED_FULL)) { + && MessageView.this.mMessage.isSet(Flag.X_DOWNLOADED_PARTIAL) + && message.isSet(Flag.X_DOWNLOADED_FULL)) { mMessageView.setHeaders(message, account); } MessageView.this.mMessage = message; @@ -1277,4 +1300,17 @@ public class MessageView extends K9Activity implements OnClickListener { mMessageView.loadBodyFromText(mAccount.getCryptoProvider(), mPgpData, mMessage, mPgpData.getDecryptedData(), "text/plain"); } + /** + * This is a {@link MessagingListener} which listens for when the a message has finished being displayed on the + * screen. We'll scroll the message to the user's last known location once it's done. + */ + class ScrollToLastLocationListener extends MessagingListener { + public void messageViewFinished() { + // Don't scroll if our last position was at the top. + if(mTopView != null && mScrollPercentage != 0.0) { + Log.d(K9.LOG_TAG, "MessageView has finished loading, scrolling to last known location."); + mTopView.setScrollPercentage(mScrollPercentage); + } + } + } } diff --git a/src/com/fsck/k9/controller/MessagingListener.java b/src/com/fsck/k9/controller/MessagingListener.java index bed951a2a..bdf35fa00 100644 --- a/src/com/fsck/k9/controller/MessagingListener.java +++ b/src/com/fsck/k9/controller/MessagingListener.java @@ -112,6 +112,11 @@ public class MessagingListener { public void loadMessageForViewFailed(Account account, String folder, String uid, Throwable t) { } + /** + * Called when a message for view has been fully displayed on the screen. + */ + public void messageViewFinished() {} + public void checkMailStarted(Context context, Account account) { } diff --git a/src/com/fsck/k9/view/AccessibleWebView.java b/src/com/fsck/k9/view/AccessibleWebView.java index 7112e6dab..5dd8bb9a8 100644 --- a/src/com/fsck/k9/view/AccessibleWebView.java +++ b/src/com/fsck/k9/view/AccessibleWebView.java @@ -26,11 +26,15 @@ import android.webkit.WebView; import android.widget.TextView; import com.fsck.k9.activity.AccessibleEmailContentActivity; +import com.fsck.k9.controller.MessagingListener; + +import java.util.Set; public class AccessibleWebView extends TextView { private Context mContext; private String mHtmlSource; private WebView mDummyWebView; + private Set mListeners = null; public AccessibleWebView(Context context) { super(context); @@ -68,6 +72,13 @@ public class AccessibleWebView extends TextView { String historyUrl) { mHtmlSource = data; this.setText(Html.fromHtml(mHtmlSource, null, null)); + + // Let everyone know that loading has finished. + if (mListeners != null) { + for (MessagingListener l : mListeners) { + l.messageViewFinished(); + } + } } public boolean zoomIn() { @@ -92,4 +103,8 @@ public class AccessibleWebView extends TextView { i.putExtra("content", mHtmlSource); mContext.startActivity(i); } + + public void setListeners(final Set listeners) { + this.mListeners = listeners; + } } diff --git a/src/com/fsck/k9/view/MessageWebView.java b/src/com/fsck/k9/view/MessageWebView.java index 9871e503e..4d22c07d6 100644 --- a/src/com/fsck/k9/view/MessageWebView.java +++ b/src/com/fsck/k9/view/MessageWebView.java @@ -1,6 +1,7 @@ package com.fsck.k9.view; import android.content.Context; +import android.graphics.Picture; import android.os.Build; import android.util.AttributeSet; import android.util.Log; @@ -10,10 +11,15 @@ import android.webkit.WebView; import android.widget.Toast; import com.fsck.k9.K9; import com.fsck.k9.R; +import com.fsck.k9.controller.MessagingListener; import java.lang.reflect.Method; +import java.util.Set; public class MessageWebView extends WebView { + // Store a reference to the listeners in MessagingController. We can't fetch it directly since + // we don't know the application name. + private Set mListeners = null; /** * We use WebSettings.getBlockNetworkLoads() to prevent the WebView that displays email @@ -97,6 +103,18 @@ public class MessageWebView extends WebView { // Disable network images by default. This is overridden by preferences. blockNetworkData(true); + + // Listen for when the screen has finished drawing. + setPictureListener(new PictureListener() { + @Override + public void onNewPicture(WebView webView, Picture picture) { + if (mListeners != null) { + for (MessagingListener l : mListeners) { + l.messageViewFinished(); + } + } + } + }); } /* @@ -115,4 +133,8 @@ public class MessageWebView extends WebView { Log.e(K9.LOG_TAG, "Exception in emulateShiftHeld()", e); } } + + public void setListeners(final Set listeners) { + this.mListeners = listeners; + } } diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java index 85afd51f3..48206f8ed 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -29,6 +29,7 @@ import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.LocalStore; import java.util.List; +import java.util.Set; /** @@ -116,11 +117,10 @@ public class SingleMessageView extends LinearLayout { return false; } - - public boolean showPictures() { return mShowPictures; } + public void setShowPictures(Boolean show) { mShowPictures = show; } @@ -312,4 +312,21 @@ public class SingleMessageView extends LinearLayout { AttachmentView.AttachmentFileDownloadCallback attachmentCallback) { this.attachmentCallback = attachmentCallback; } + + /** + * Save a copy of the {@link com.fsck.k9.controller.MessagingController#getListeners()}. This method will also + * pass along these listeners to the underlying views. + * @param listeners Set of listeners. + */ + public void setListeners(final Set listeners) { + if(!mScreenReaderEnabled) { + if(mMessageContentView != null) { + mMessageContentView.setListeners(listeners); + } + } else { + if(mAccessibleMessageContentView != null) { + mAccessibleMessageContentView.setListeners(listeners); + } + } + } } diff --git a/src/com/fsck/k9/view/ToggleScrollView.java b/src/com/fsck/k9/view/ToggleScrollView.java index 16ee41ab1..e0f3cd466 100644 --- a/src/com/fsck/k9/view/ToggleScrollView.java +++ b/src/com/fsck/k9/view/ToggleScrollView.java @@ -2,13 +2,19 @@ package com.fsck.k9.view; import android.content.Context; import android.util.AttributeSet; +import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.widget.ScrollView; +import com.fsck.k9.K9; +/** + * An extension of {@link ScrollView} that allows scrolling to be selectively disabled. + */ public class ToggleScrollView extends ScrollView { private GestureDetector mDetector; private boolean mScrolling = true; + private int currentYPosition; public ToggleScrollView(Context context, AttributeSet attrs) { super(context, attrs); @@ -59,4 +65,44 @@ public class ToggleScrollView extends ScrollView { return false; } } + + /** + * Fetch the current percentage by which this view has been scrolled. + * @return Scroll percentage based on the top edge of the screen, from 0 to 100. This number should never really + * be 100, unless the screen is of 0 height... + */ + public double getScrollPercentage() { + if(computeVerticalScrollRange() == 0) { + return 0; + } + return (double)currentYPosition / computeVerticalScrollRange(); + } + + /** + * Scroll the screen to a specific percentage of the page. This is based on the top edge of the page. + * @param percentage Percentage of page to scroll to. + */ + public void setScrollPercentage(final double percentage) { + final int newY = (int)(percentage * computeVerticalScrollRange()); + Log.d(K9.LOG_TAG, "ToggleScrollView: requested " + (100 * percentage) + "%, scrolling to " + newY + "/" + computeVerticalScrollRange()); + scrollTo(0, newY); + } + + /** + * Override {@link ScrollView#onScrollChanged(int, int, int, int)} to record the current x/y position. We use this + * to save our current position for future scrolling. + * + * @param x + * @param y + * @param oldx + * @param oldy + */ + @Override + protected void onScrollChanged(int x, int y, int oldx, int oldy) { + super.onScrollChanged(x, y, oldx, oldy); + + this.currentYPosition = y; + // I wish Android has a TRACE log level so I wouldn't have to comment this out. This one is really noisy. + // Log.d(K9.LOG_TAG, "ToggleScrollView: currentYPosition=" + y + " scrollRange=" + computeVerticalScrollRange() + " pct=" + getScrollPercentage()); + } } From f6eea9f01486712c58137983f8fe55b5fb2de056 Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Thu, 3 Nov 2011 09:14:42 -0700 Subject: [PATCH 2/2] Move the listener logic out of the Activity and into the View. Change variable naming in ToggleScrollView to meet standard. Cache the result of computeVerticalScrollRange() in ToggleScrollView. --- src/com/fsck/k9/activity/MessageView.java | 40 ++++------------ src/com/fsck/k9/view/ToggleScrollView.java | 53 ++++++++++++++++++---- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java index 58efce0ef..cdb7abb8e 100644 --- a/src/com/fsck/k9/activity/MessageView.java +++ b/src/com/fsck/k9/activity/MessageView.java @@ -97,7 +97,6 @@ public class MessageView extends K9Activity implements OnClickListener { private Listener mListener = new Listener(); private MessageViewHandler mHandler = new MessageViewHandler(); private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation(); - private MessagingListener mLoadCompleteListener = new ScrollToLastLocationListener(); /** this variable is used to save the calling AttachmentView * until the onActivityResult is called. @@ -111,11 +110,6 @@ public class MessageView extends K9Activity implements OnClickListener { */ private String mDstFolder; - /** - * Used after restore/rotation to scroll our message to the last known location. - */ - private double mScrollPercentage; - private final class StorageListenerImplementation implements StorageManager.StorageListener { @Override public void onUnmount(String providerId) { @@ -418,9 +412,8 @@ public class MessageView extends K9Activity implements OnClickListener { mMessageView.initialize(this); - // Add listener for message load completion. We'll use this to scroll the page to user's last known - // location. - mController.addListener(mLoadCompleteListener); + // Register the ScrollView's listener to handle scrolling to last known location on resume. + mController.addListener(mTopView.getListener()); mMessageView.setListeners(mController.getListeners()); setTitle(""); @@ -430,6 +423,7 @@ public class MessageView extends K9Activity implements OnClickListener { Uri uri = intent.getData(); if (icicle != null) { + // TODO This code seems unnecessary since the icicle should already be thawed in onRestoreInstanceState(). mMessageReference = icicle.getParcelable(EXTRA_MESSAGE_REFERENCE); mMessageReferences = icicle.getParcelableArrayList(EXTRA_MESSAGE_REFERENCES); mPgpData = (PgpData) icicle.getSerializable(STATE_PGP_DATA); @@ -543,9 +537,7 @@ public class MessageView extends K9Activity implements OnClickListener { outState.putParcelableArrayList(EXTRA_MESSAGE_REFERENCES, mMessageReferences); outState.putSerializable(STATE_PGP_DATA, mPgpData); outState.putBoolean(SHOW_PICTURES, mMessageView.showPictures()); - if(mTopView != null) { - outState.putDouble(EXTRA_SCROLL_PERCENTAGE, mTopView.getScrollPercentage()); - } + outState.putDouble(EXTRA_SCROLL_PERCENTAGE, mTopView.getScrollPercentage()); } @Override @@ -554,7 +546,7 @@ public class MessageView extends K9Activity implements OnClickListener { mPgpData = (PgpData) savedInstanceState.getSerializable(STATE_PGP_DATA); mMessageView.updateCryptoLayout(mAccount.getCryptoProvider(), mPgpData, mMessage); mMessageView.setLoadPictures(savedInstanceState.getBoolean(SHOW_PICTURES)); - mScrollPercentage = savedInstanceState.getDouble(EXTRA_SCROLL_PERCENTAGE); + mTopView.setScrollPercentage(savedInstanceState.getDouble(EXTRA_SCROLL_PERCENTAGE)); } private void displayMessage(MessageReference ref) { @@ -674,13 +666,13 @@ public class MessageView extends K9Activity implements OnClickListener { onAccountUnavailable(); return; } - mController.addListener(mLoadCompleteListener); + mController.addListener(mTopView.getListener()); StorageManager.getInstance(getApplication()).addListener(mStorageListener); } @Override protected void onPause() { - mController.removeListener(mLoadCompleteListener); + mController.removeListener(mTopView.getListener()); StorageManager.getInstance(getApplication()).removeListener(mStorageListener); super.onPause(); } @@ -893,7 +885,7 @@ public class MessageView extends K9Activity implements OnClickListener { @Override protected void onNext() { // Reset scroll percentage when we change messages - mScrollPercentage = 0; + mTopView.setScrollPercentage(0); if (mNextMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; @@ -910,7 +902,7 @@ public class MessageView extends K9Activity implements OnClickListener { @Override protected void onPrevious() { // Reset scroll percentage when we change messages - mScrollPercentage = 0; + mTopView.setScrollPercentage(0); if (mPreviousMessage == null) { Toast.makeText(this, getString(R.string.end_of_folder), Toast.LENGTH_SHORT).show(); return; @@ -1299,18 +1291,4 @@ public class MessageView extends K9Activity implements OnClickListener { // sometimes shows the original encrypted content mMessageView.loadBodyFromText(mAccount.getCryptoProvider(), mPgpData, mMessage, mPgpData.getDecryptedData(), "text/plain"); } - - /** - * This is a {@link MessagingListener} which listens for when the a message has finished being displayed on the - * screen. We'll scroll the message to the user's last known location once it's done. - */ - class ScrollToLastLocationListener extends MessagingListener { - public void messageViewFinished() { - // Don't scroll if our last position was at the top. - if(mTopView != null && mScrollPercentage != 0.0) { - Log.d(K9.LOG_TAG, "MessageView has finished loading, scrolling to last known location."); - mTopView.setScrollPercentage(mScrollPercentage); - } - } - } } diff --git a/src/com/fsck/k9/view/ToggleScrollView.java b/src/com/fsck/k9/view/ToggleScrollView.java index e0f3cd466..714e8ce24 100644 --- a/src/com/fsck/k9/view/ToggleScrollView.java +++ b/src/com/fsck/k9/view/ToggleScrollView.java @@ -7,6 +7,7 @@ import android.view.GestureDetector; import android.view.MotionEvent; import android.widget.ScrollView; import com.fsck.k9.K9; +import com.fsck.k9.controller.MessagingListener; /** * An extension of {@link ScrollView} that allows scrolling to be selectively disabled. @@ -14,7 +15,9 @@ import com.fsck.k9.K9; public class ToggleScrollView extends ScrollView { private GestureDetector mDetector; private boolean mScrolling = true; - private int currentYPosition; + private int mCurrentYPosition; + private double mScrollPercentage; + private ScrollToLastLocationListener mListener; public ToggleScrollView(Context context, AttributeSet attrs) { super(context, attrs); @@ -72,20 +75,23 @@ public class ToggleScrollView extends ScrollView { * be 100, unless the screen is of 0 height... */ public double getScrollPercentage() { - if(computeVerticalScrollRange() == 0) { + // We save only the Y coordinate instead of the percentage because I don't know how expensive the + // computeVerticalScrollRange() call is. + final int scrollRange = computeVerticalScrollRange(); + if(scrollRange == 0) { return 0; } - return (double)currentYPosition / computeVerticalScrollRange(); + return (double) mCurrentYPosition / scrollRange; } /** - * Scroll the screen to a specific percentage of the page. This is based on the top edge of the page. + * Set the percentage by which we should scroll the page once we get the load complete event. This is + * based on the top edge of the view. * @param percentage Percentage of page to scroll to. */ public void setScrollPercentage(final double percentage) { - final int newY = (int)(percentage * computeVerticalScrollRange()); - Log.d(K9.LOG_TAG, "ToggleScrollView: requested " + (100 * percentage) + "%, scrolling to " + newY + "/" + computeVerticalScrollRange()); - scrollTo(0, newY); + Log.d(K9.LOG_TAG, "ToggleView: Setting last scroll percentage to " + percentage); + this.mScrollPercentage = percentage; } /** @@ -101,8 +107,37 @@ public class ToggleScrollView extends ScrollView { protected void onScrollChanged(int x, int y, int oldx, int oldy) { super.onScrollChanged(x, y, oldx, oldy); - this.currentYPosition = y; + this.mCurrentYPosition = y; // I wish Android has a TRACE log level so I wouldn't have to comment this out. This one is really noisy. - // Log.d(K9.LOG_TAG, "ToggleScrollView: currentYPosition=" + y + " scrollRange=" + computeVerticalScrollRange() + " pct=" + getScrollPercentage()); + // Log.d(K9.LOG_TAG, "ToggleScrollView: mCurrentYPosition=" + y + " scrollRange=" + computeVerticalScrollRange() + " pct=" + getScrollPercentage()); + } + + /** + * This is a {@link MessagingListener} which listens for when the a message has finished being displayed on the + * screen. We'll scroll the message to the user's last known location once it's done. + */ + class ScrollToLastLocationListener extends MessagingListener { + public void messageViewFinished() { + // Don't scroll if our last position was at the top. + if(mScrollPercentage != 0.0) { + final int scrollRange = computeVerticalScrollRange(); + final int newY = (int)(mScrollPercentage * scrollRange); + Log.d(K9.LOG_TAG, "ToggleScrollView: requested " + (100 * mScrollPercentage) + "%, " + + "scrolling to " + newY + "/" + scrollRange); + scrollTo(0, newY); + } + } + } + + /** + * Fetch the {@link MessagingListener} for this ScrollView. + * @return + */ + public MessagingListener getListener() { + if(this.mListener != null) { + return this.mListener; + } else { + return this.mListener = new ScrollToLastLocationListener(); + } } }