From 0b69338a45faa422ccba8faf64c9816c55d33e4a Mon Sep 17 00:00:00 2001 From: Jin Cao Date: Mon, 11 Aug 2014 10:46:12 -0700 Subject: [KBNav] two-pane landscape navigation polish The system default navigation doesn't work quite as well in landscape mode (e.g. focus on conversation view's reply, hitting up goes to the mini-drawer instead of the message header). This is partly because our overlay views are not in a real list, thus we can't take advantage of the framework's navigation support for listviews. I decided to roll my own navigation entirely for conversation view and manually focus/scroll. This CL also includes some polishes for interactions in landscape mode between drawer, TL, and CV panes. b/16636060 Change-Id: Id1de01439a118702756d52f6a8b3f02395a0f932 --- .../android/mail/browse/ConversationContainer.java | 22 +++++ .../mail/browse/ConversationOverlayItem.java | 8 ++ .../mail/browse/ConversationPagerController.java | 5 + .../mail/browse/ConversationViewAdapter.java | 106 ++++++++++++++++++++- .../mail/ui/AbstractActivityController.java | 2 +- src/com/android/mail/ui/ActivityController.java | 2 +- src/com/android/mail/ui/ControllableActivity.java | 2 + .../android/mail/ui/ConversationListFragment.java | 10 +- .../android/mail/ui/ConversationViewFragment.java | 93 +++++++++++++++--- .../android/mail/ui/FolderSelectionActivity.java | 6 ++ .../mail/ui/KeyboardNavigationController.java | 34 +++++++ src/com/android/mail/ui/MailActivity.java | 5 + src/com/android/mail/ui/OnePaneController.java | 13 +++ src/com/android/mail/ui/TwoPaneController.java | 32 +++++-- src/com/android/mail/ui/TwoPaneLayout.java | 23 ++--- 15 files changed, 327 insertions(+), 36 deletions(-) create mode 100644 src/com/android/mail/ui/KeyboardNavigationController.java diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java index f99bd3731..05faad17b 100644 --- a/src/com/android/mail/browse/ConversationContainer.java +++ b/src/com/android/mail/browse/ConversationContainer.java @@ -20,6 +20,7 @@ package com.android.mail.browse; import android.content.Context; import android.content.res.Configuration; import android.database.DataSetObserver; +import android.support.annotation.IdRes; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.SparseArray; @@ -994,6 +995,27 @@ public class ConversationContainer extends ViewGroup implements ScrollListener { LogUtils.d(TAG, msg, params); } + public void focusFirstMessageHeader() { + mOverlayAdapter.focusFirstMessageHeader(); + } + + public int getViewPosition(View v) { + return mOverlayAdapter.getViewPosition(v); + } + + public View getNextOverlayView(int position, boolean isDown) { + return mOverlayAdapter.getNextOverlayView(position, isDown); + } + + public boolean shouldInterceptLeftRightEvents(@IdRes int id, boolean isLeft, boolean isRight, + boolean twoPaneLand) { + return mOverlayAdapter.shouldInterceptLeftRightEvents(id, isLeft, isRight, twoPaneLand); + } + + public boolean shouldNavigateAway(@IdRes int id, boolean isLeft, boolean twoPaneLand) { + return mOverlayAdapter.shouldNavigateAway(id, isLeft, twoPaneLand); + } + private class AdapterObserver extends DataSetObserver { @Override public void onChanged() { diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java index 08f2cdfba..6850dd113 100644 --- a/src/com/android/mail/browse/ConversationOverlayItem.java +++ b/src/com/android/mail/browse/ConversationOverlayItem.java @@ -38,6 +38,9 @@ public abstract class ConversationOverlayItem { private int mPosition; + // The view to focus when this overlay item should be focused. + protected View mRootView; + /** * @see Adapter#getItemViewType(int) */ @@ -188,6 +191,11 @@ public abstract class ConversationOverlayItem { // DO NOTHING } + public View getFocusableView() { + // Focus the root view by default + return mRootView; + } + public void registerOnKeyListeners(View... views) { final View.OnKeyListener listener = getOnKeyListener(); if (listener != null) { diff --git a/src/com/android/mail/browse/ConversationPagerController.java b/src/com/android/mail/browse/ConversationPagerController.java index ae576bda8..347b4ae95 100644 --- a/src/com/android/mail/browse/ConversationPagerController.java +++ b/src/com/android/mail/browse/ConversationPagerController.java @@ -157,6 +157,11 @@ public class ConversationPagerController { cleanup(); } + // Explicitly set the focus to the conversation pager, specifically the conv overlay. + public void focusPager() { + mPager.requestFocus(); + } + public boolean isInitialConversationLoading() { return mInitialConversationLoading; } diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java index bda63c136..14da20367 100644 --- a/src/com/android/mail/browse/ConversationViewAdapter.java +++ b/src/com/android/mail/browse/ConversationViewAdapter.java @@ -20,12 +20,14 @@ package com.android.mail.browse; import android.app.FragmentManager; import android.app.LoaderManager; import android.content.Context; +import android.support.annotation.IdRes; import android.support.annotation.IntDef; import android.support.v4.text.BidiFormatter; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.widget.BaseAdapter; import com.android.emailcommon.mail.Address; @@ -67,6 +69,7 @@ import java.util.Map; public class ConversationViewAdapter extends BaseAdapter { private static final String LOG_TAG = LogTag.getLogTag(); + private static final String OVERLAY_ITEM_ROOT_TAG = "overlay_item_root"; private final Context mContext; private final FormattedDateBuilder mDateBuilder; @@ -130,13 +133,13 @@ public class ConversationViewAdapter extends BaseAdapter { R.layout.conversation_view_header, parent, false); v.setCallbacks( mConversationCallbacks, mAccountController, mConversationUpdater); - v.bind(this); v.setSubject(mConversation.subject); if (mAccountController.getAccount().supportsCapability( UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) { v.setFolders(mConversation); } v.setStarred(mConversation.starred); + v.setTag(OVERLAY_ITEM_ROOT_TAG); // Register the onkey listener for all relevant views registerOnKeyListeners(v, v.findViewById(R.id.subject_and_folder_view)); @@ -148,6 +151,7 @@ public class ConversationViewAdapter extends BaseAdapter { public void bindView(View v, boolean measureOnly) { ConversationViewHeader header = (ConversationViewHeader) v; header.bind(this); + mRootView = v; } @Override @@ -183,6 +187,7 @@ public class ConversationViewAdapter extends BaseAdapter { inflater.inflate(R.layout.conversation_footer, parent, false); v.setAccountController(mAccountController); v.setConversationFooterCallbacks(mConversationFooterCallbacks); + v.setTag(OVERLAY_ITEM_ROOT_TAG); // Register the onkey listener for all relevant views registerOnKeyListeners(v, v.findViewById(R.id.reply_button), @@ -194,11 +199,18 @@ public class ConversationViewAdapter extends BaseAdapter { @Override public void bindView(View v, boolean measureOnly) { ((ConversationFooterView) v).bind(this); + mRootView = v; } @Override public void rebindView(View view) { ((ConversationFooterView) view).rebind(this); + mRootView = view; + } + + @Override + public View getFocusableView() { + return mRootView.findViewById(R.id.reply_button); } @Override @@ -268,6 +280,7 @@ public class ConversationViewAdapter extends BaseAdapter { v.setCallbacks(mAdapter.mMessageCallbacks); v.setContactInfoSource(mAdapter.mContactInfoSource); v.setVeiledMatcher(mAdapter.mMatcher); + v.setTag(OVERLAY_ITEM_ROOT_TAG); // Register the onkey listener for all relevant views registerOnKeyListeners(v, v.findViewById(R.id.upper_header), @@ -281,6 +294,12 @@ public class ConversationViewAdapter extends BaseAdapter { public void bindView(View v, boolean measureOnly) { final MessageHeaderView header = (MessageHeaderView) v; header.bind(this, measureOnly); + mRootView = v; + } + + @Override + public View getFocusableView() { + return mRootView.findViewById(R.id.upper_header); } @Override @@ -370,6 +389,7 @@ public class ConversationViewAdapter extends BaseAdapter { public void rebindView(View view) { final MessageHeaderView header = (MessageHeaderView) view; header.rebind(this); + mRootView = view; } } @@ -398,6 +418,7 @@ public class ConversationViewAdapter extends BaseAdapter { R.layout.conversation_message_footer, parent, false); v.initialize(mAdapter.mLoaderManager, mAdapter.mFragmentManager, mAdapter.mAccountController, mAdapter.mFooterCallbacks); + v.setTag(OVERLAY_ITEM_ROOT_TAG); // Register the onkey listener for all relevant views registerOnKeyListeners(v, v.findViewById(R.id.view_entire_message_prompt)); @@ -408,6 +429,7 @@ public class ConversationViewAdapter extends BaseAdapter { public void bindView(View v, boolean measureOnly) { final MessageFooterView attachmentsView = (MessageFooterView) v; attachmentsView.bind(mHeaderItem, measureOnly); + mRootView = v; } @Override @@ -470,6 +492,7 @@ public class ConversationViewAdapter extends BaseAdapter { R.layout.super_collapsed_block, parent, false); v.initialize(mSuperCollapsedListener); v.setOnKeyListener(mOnKeyListener); + v.setTag(OVERLAY_ITEM_ROOT_TAG); // Register the onkey listener for all relevant views registerOnKeyListeners(v); @@ -480,6 +503,7 @@ public class ConversationViewAdapter extends BaseAdapter { public void bindView(View v, boolean measureOnly) { final SuperCollapsedBlock scb = (SuperCollapsedBlock) v; scb.bind(this); + mRootView = v; } @Override @@ -712,6 +736,86 @@ public class ConversationViewAdapter extends BaseAdapter { && mItems.get(position).getType() == VIEW_TYPE_SUPER_COLLAPSED_BLOCK; } + // This should be a safe call since all containers should have at least a conv header and a + // message header. + // TODO: what to do when the first header is off the screen and recycled? + public void focusFirstMessageHeader() { + if (mItems.size() > 1) { + final View v = mItems.get(1).getFocusableView(); + if (v != null) { + v.requestFocus(); + } + } + } + + /** + * Try to find the position of the provided view (or it's view container) in the adapter. + */ + public int getViewPosition(View v) { + // First find the root view of the overlay item + while (v.getTag() != OVERLAY_ITEM_ROOT_TAG) { + final ViewParent parent = v.getParent(); + if (parent != null && parent instanceof View) { + v = (View) parent; + } else { + return -1; + } + } + // Find the position of the root view + for (int i = 0; i < mItems.size(); i++) { + if (mItems.get(i).mRootView == v) { + return i; + } + } + return -1; + } + + /** + * Find the next view that should grab focus with respect to the current position. + */ + public View getNextOverlayView(int position, boolean isDown) { + if (isDown && position >= 0) { + while (++position < mItems.size()) { + final View v = mItems.get(position).getFocusableView(); + if (v != null && v.isFocusable()) { + return v; + } + } + } else { + while (--position >= 0) { + final View v = mItems.get(position).getFocusableView(); + if (v != null && v.isFocusable()) { + return v; + } + } + } + return null; + } + + public boolean shouldInterceptLeftRightEvents(@IdRes int id, boolean isLeft, boolean isRight, + boolean twoPaneLand) { + return twoPaneLand && (id == R.id.conversation_header || + id == R.id.subject_and_folder_view || + id == R.id.upper_header || + id == R.id.super_collapsed_block || + id == R.id.message_footer || + (id == R.id.overflow && isRight) || + (id == R.id.reply_button && isLeft) || + (id == R.id.forward_button && isRight)); + } + + // Indicates if the direction with the provided id should navigate away from the conversation + // view. Note that this is only applicable in two-pane landscape mode. + public boolean shouldNavigateAway(@IdRes int id, boolean isLeft, boolean twoPaneLand) { + return twoPaneLand && isLeft && + (id == R.id.conversation_header || + id == R.id.subject_and_folder_view || + id == R.id.upper_header || + id == R.id.super_collapsed_block || + id == R.id.message_footer || + id == R.id.reply_button); + } + public BidiFormatter getBidiFormatter() { return mBidiFormatter; } diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java index f6727d38a..6d75402cd 100644 --- a/src/com/android/mail/ui/AbstractActivityController.java +++ b/src/com/android/mail/ui/AbstractActivityController.java @@ -2540,7 +2540,7 @@ public abstract class AbstractActivityController implements ActivityController, } @Override - public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { + public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { final ConversationListFragment convListFragment = getConversationListFragment(); if (convListFragment != null && convListFragment.getAnimatedAdapter() != null) { convListFragment.getAnimatedAdapter().onConversationSelected(); diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java index 9af3e3987..fec109b4b 100644 --- a/src/com/android/mail/ui/ActivityController.java +++ b/src/com/android/mail/ui/ActivityController.java @@ -63,7 +63,7 @@ public interface ActivityController extends LayoutListener, FolderChangeListener, ConversationSetObserver, ConversationListener, FolderSelector, UndoListener, ConversationUpdater, ErrorListener, FolderController, AccountController, ConversationPositionTracker.Callbacks, ConversationListFooterView.FooterViewClickListener, - RecentFolderController, FragmentLauncher { + RecentFolderController, FragmentLauncher, KeyboardNavigationController { // As far as possible, the methods here that correspond to Activity lifecycle have the same name // as their counterpart in the Activity lifecycle. diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java index 0600be697..b9995b947 100644 --- a/src/com/android/mail/ui/ControllableActivity.java +++ b/src/com/android/mail/ui/ControllableActivity.java @@ -101,6 +101,8 @@ public interface ControllableActivity extends RestrictedActivity, DrawerController getDrawerController(); + KeyboardNavigationController getKeyboardNavigationController(); + void startDragMode(); void stopDragMode(); diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java index 9192de24d..177d38eb0 100644 --- a/src/com/android/mail/ui/ConversationListFragment.java +++ b/src/com/android/mail/ui/ConversationListFragment.java @@ -156,6 +156,9 @@ public final class ConversationListFragment extends Fragment implements private boolean mInitialCursorLoading; private @IdRes int mNextFocusLeftId; + // Tracks if a onKey event was initiated from the listview (received ACTION_DOWN before + // ACTION_UP). If not, the listview only receives ACTION_UP. + private boolean mKeyInitiatedFromList; /** Duration, in milliseconds, of the CAB mode (peek icon) animation. */ private static long sSelectionModeAnimationDuration = -1; @@ -627,7 +630,12 @@ public final class ConversationListFragment extends Fragment implements // Don't need to handle ENTER because it's auto-handled as a "click". if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { if (keyEvent.getAction() == KeyEvent.ACTION_UP) { - onListItemSelected(list.getSelectedView(), list.getSelectedItemPosition()); + if (mKeyInitiatedFromList) { + onListItemSelected(list.getSelectedView(), list.getSelectedItemPosition()); + } + mKeyInitiatedFromList = false; + } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + mKeyInitiatedFromList = true; } return true; } else if (keyEvent.getAction() == KeyEvent.ACTION_UP) { diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java index 803574ffc..a24b4d404 100644 --- a/src/com/android/mail/ui/ConversationViewFragment.java +++ b/src/com/android/mail/ui/ConversationViewFragment.java @@ -23,6 +23,7 @@ import android.content.Loader; import android.content.res.Resources; import android.database.Cursor; import android.database.DataSetObserver; +import android.graphics.Rect; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -127,6 +128,18 @@ public class ConversationViewFragment extends AbstractConversationViewFragment i */ private final int LOAD_WAIT_UNTIL_VISIBLE = 2; + // Keyboard navigation + private KeyboardNavigationController mNavigationController; + // Since we manually control navigation for most of the conversation view due to problems + // with two-pane layout but still rely on the system for SOME navigation, we need to keep track + // of the view that had focus when KeyEvent.ACTION_DOWN was fired. This is because we only + // manually change focus on KeyEvent.ACTION_UP (to prevent holding down the DOWN button and + // lagging the app), however, the view in focus might have changed between ACTION_UP and + // ACTION_DOWN since the system might have handled the ACTION_DOWN and moved focus. + private View mOriginalKeyedView; + private int mMaxScreenHeight; + private int mTopOfVisibleScreen; + protected ConversationContainer mConversationContainer; protected ConversationWebView mWebView; @@ -277,6 +290,8 @@ public class ConversationViewFragment extends AbstractConversationViewFragment i final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context); + mNavigationController = mActivity.getKeyboardNavigationController(); + mAdapter = new ConversationViewAdapter(mActivity, this, getLoaderManager(), this, this, getContactInfoSource(), this, this, getListController(), this, mAddressCache, dateBuilder, mBidiFormatter, this); @@ -329,6 +344,12 @@ public class ConversationViewFragment extends AbstractConversationViewFragment i new SetCookieTask(getContext(), mConversation.conversationBaseUri.toString(), mAccount.accountCookieQueryUri).execute(); } + + // Find the height of the screen for manually scrolling the webview via keyboard. + final Rect screen = new Rect(); + mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(screen); + mMaxScreenHeight = screen.bottom; + mTopOfVisibleScreen = screen.top + mActivity.getSupportActionBar().getHeight(); } @Override @@ -1121,20 +1142,68 @@ public class ConversationViewFragment extends AbstractConversationViewFragment i @Override public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { - // Only care about enter and esc - View currFocus = mActivity.getWindow().getCurrentFocus(); - if (keyCode == KeyEvent.KEYCODE_BACK && currFocus != null && - currFocus.getId() != R.id.conversation_topmost_overlay) { - if (keyEvent.getAction() == KeyEvent.ACTION_UP) { - mTopmostOverlay.requestFocus(); + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + mOriginalKeyedView = view; + } + + if (mOriginalKeyedView != null) { + final int id = mOriginalKeyedView.getId(); + final boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; + final boolean isLeft = keyCode == KeyEvent.KEYCODE_DPAD_LEFT; + final boolean isRight = keyCode == KeyEvent.KEYCODE_DPAD_RIGHT; + final boolean isUp = keyCode == KeyEvent.KEYCODE_DPAD_UP; + final boolean isDown = keyCode == KeyEvent.KEYCODE_DPAD_DOWN; + + // First we run the event by the controller + // We manually check if the view+direction combination should shift focus away from the + // conversation view to the thread list in two-pane landscape mode. + final boolean isTwoPaneLand = mNavigationController.isTwoPaneLandscape(); + final boolean navigateAway = mConversationContainer.shouldNavigateAway(id, isLeft, + isTwoPaneLand); + if (mNavigationController.onInterceptKeyFromCV(keyCode, keyEvent, navigateAway)) { + return true; } - return true; - } else if (keyCode == KeyEvent.KEYCODE_ENTER && (currFocus == null || - currFocus.getId() == R.id.conversation_topmost_overlay)) { - if (keyEvent.getAction() == KeyEvent.ACTION_UP) { - mConversationContainer.findViewById(R.id.upper_header).requestFocus(); + + // If controller didn't handle the event, check directional interception. + if ((isLeft || isRight) && mConversationContainer.shouldInterceptLeftRightEvents( + id, isLeft, isRight, isTwoPaneLand)) { + return true; + } else if (isUp || isDown) { + // We manually handle up/down navigation through the overlay items because the + // system's default isn't optimal for two-pane landscape since it's not a real list. + final int position = mConversationContainer.getViewPosition(mOriginalKeyedView); + final View next = mConversationContainer.getNextOverlayView(position, isDown); + if (next != null) { + if (isActionUp) { + next.requestFocus(); + + // Make sure that v is in view + final int[] coords = new int[2]; + next.getLocationOnScreen(coords); + final int bottom = coords[1] + next.getHeight(); + if (bottom > mMaxScreenHeight) { + mWebView.scrollBy(0, bottom - mMaxScreenHeight); + } else if (coords[1] < mTopOfVisibleScreen) { + mWebView.scrollBy(0, coords[1] - mTopOfVisibleScreen); + } + } + return true; + } + } + + // Finally we handle the special keys + if (keyCode == KeyEvent.KEYCODE_BACK && id != R.id.conversation_topmost_overlay) { + if (isActionUp) { + mTopmostOverlay.requestFocus(); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_ENTER && + id == R.id.conversation_topmost_overlay) { + if (isActionUp) { + mConversationContainer.focusFirstMessageHeader(); + } + return true; } - return true; } return false; } diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java index 93594b5ed..96b06496f 100644 --- a/src/com/android/mail/ui/FolderSelectionActivity.java +++ b/src/com/android/mail/ui/FolderSelectionActivity.java @@ -436,6 +436,12 @@ public class FolderSelectionActivity extends ActionBarActivity implements OnClic return null; } + @Override + public KeyboardNavigationController getKeyboardNavigationController() { + // Unsupported + return null; + } + @Override public boolean isAccessibilityEnabled() { // Unsupported diff --git a/src/com/android/mail/ui/KeyboardNavigationController.java b/src/com/android/mail/ui/KeyboardNavigationController.java new file mode 100644 index 000000000..768133a47 --- /dev/null +++ b/src/com/android/mail/ui/KeyboardNavigationController.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 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.ui; + +import android.view.KeyEvent; + +/** + * Callbacks for cross-fragment keyboard navigation functionality. + */ +public interface KeyboardNavigationController { + /** + * Intercept a key press from ConversationViewFragment. + * @param navigateAway custom param indicating if the controller should navigate away from + * the conversation view. + * @return true if the event was handled by the controller, false otherwise. + */ + public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway); + public boolean isTwoPaneLandscape(); +} diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java index 0c102241b..78507dad1 100644 --- a/src/com/android/mail/ui/MailActivity.java +++ b/src/com/android/mail/ui/MailActivity.java @@ -417,6 +417,11 @@ public class MailActivity extends AbstractMailActivity implements ControllableAc return mController.getDrawerController(); } + @Override + public KeyboardNavigationController getKeyboardNavigationController() { + return mController; + } + @Override public void onFooterViewErrorActionClick(Folder folder, int errorStatus) { mController.onFooterViewErrorActionClick(folder, errorStatus); diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java index 0d83572c6..0d0e58dec 100644 --- a/src/com/android/mail/ui/OnePaneController.java +++ b/src/com/android/mail/ui/OnePaneController.java @@ -23,9 +23,11 @@ import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.v4.widget.DrawerLayout; import android.view.Gravity; +import android.view.KeyEvent; import android.view.View; import android.widget.ListView; @@ -500,4 +502,15 @@ public final class OnePaneController extends AbstractActivityController { replaceFragment(fragment, FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_CUSTOM_FRAGMENT, R.id.content_pane); } + + @Override + public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) { + // Not applicable + return false; + } + + @Override + public boolean isTwoPaneLandscape() { + return false; + } } diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java index 7e03f1030..3be51ff22 100644 --- a/src/com/android/mail/ui/TwoPaneController.java +++ b/src/com/android/mail/ui/TwoPaneController.java @@ -21,11 +21,13 @@ import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.app.ListFragment; import android.content.Intent; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.v7.app.ActionBar; +import android.view.KeyEvent; import android.view.View; import android.widget.ListView; @@ -43,7 +45,7 @@ import com.android.mail.utils.Utils; * abounds. */ public final class TwoPaneController extends AbstractActivityController implements - ConversationViewFrame.DownEventListener, TwoPaneLayout.TwoPaneLayoutListener { + ConversationViewFrame.DownEventListener { private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view"; private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID = @@ -157,7 +159,6 @@ public final class TwoPaneController extends AbstractActivityController implemen return false; } mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())); - mLayout.setLayoutListener(this); mActivity.getWindow().setBackgroundDrawable(null); mIsTabletLandscape = !mActivity.getResources().getBoolean(R.bool.list_collapsible); @@ -397,6 +398,13 @@ public final class TwoPaneController extends AbstractActivityController implemen } } + @Override + public final void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { + super.onConversationSelected(conversation, inLoaderCallbacks); + // Shift the focus to the conversation in landscape mode + mPagerController.focusPager(); + } + @Override public void onConversationFocused(Conversation conversation) { if (mIsTabletLandscape) { @@ -606,10 +614,22 @@ public final class TwoPaneController extends AbstractActivityController implemen } @Override - public void onListViewLayout(boolean isOnScreen) { - ConversationListFragment clf = getConversationListFragment(); - if (clf != null && clf.getListView() != null) { - clf.getListView().setFocusable(isOnScreen); + public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) { + // Override left/right key presses in landscape mode. + if (navigateAway) { + if (keyEvent.getAction() == KeyEvent.ACTION_UP) { + ConversationListFragment clf = getConversationListFragment(); + if (clf != null) { + clf.getListView().requestFocus(); + } + } + return true; } + return false; + } + + @Override + public boolean isTwoPaneLandscape() { + return mIsTabletLandscape; } } diff --git a/src/com/android/mail/ui/TwoPaneLayout.java b/src/com/android/mail/ui/TwoPaneLayout.java index 5f29664a7..6dfa711ef 100644 --- a/src/com/android/mail/ui/TwoPaneLayout.java +++ b/src/com/android/mail/ui/TwoPaneLayout.java @@ -96,8 +96,6 @@ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { private View mFoldersView; private View mListView; - private TwoPaneLayoutListener mLayoutListener; - public static final int MISCELLANEOUS_VIEW_ID = R.id.miscellaneous_pane; private final Runnable mTransitionCompleteRunnable = new Runnable() { @@ -173,10 +171,6 @@ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { super.onLayout(changed, l, t, r, b); } - public void setLayoutListener(TwoPaneLayoutListener listener) { - mLayoutListener = listener; - } - /** * Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes * have the correct widths set for the current overall size and view mode. @@ -206,6 +200,7 @@ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { final int foldersW = isDrawerOpen() ? mDrawerWidthOpen : mDrawerWidthMini; final int listW = getPaneWidth(mListView); + boolean cvOnScreen = true; if (!mListCollapsible) { if (isRtl) { foldersX = width - mDrawerWidthOpen; @@ -231,6 +226,7 @@ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { } } else { // TL mode + cvOnScreen = false; if (isRtl) { foldersX = width - mDrawerWidthOpen; listX = width - foldersW - listW; @@ -244,10 +240,13 @@ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { } animatePanes(foldersX, listX, convX); - // For keyboard navigation, let's disable focus on the list if it's not visible. - if (mLayoutListener != null) { - mLayoutListener.onListViewLayout(listX >= 0); - } + + // For views that are not on the screen, let's set their visibility for accessibility. + mFoldersView.setVisibility(foldersX >= 0 ? VISIBLE : INVISIBLE); + mListView.setVisibility(listX >= 0 ? VISIBLE : INVISIBLE); + mConversationView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE); + mMiscellaneousView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE); + mPositionedMode = mCurrentMode; } @@ -465,8 +464,4 @@ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { public boolean shouldShowPreviewPanel() { return !mListCollapsible; } - - public interface TwoPaneLayoutListener { - public void onListViewLayout(boolean isOnScreen); - } } -- cgit v1.2.3