PreferenceFragment cannot scroll up/down on XE16 (

2019-03-31 01:49发布

问题:

I have a few user preferences, mostly simple check-boxes, in my Glass GDK app. I could not find a glass specific preference paradigm, so I used PreferenceFragment and it worked fine on XE12.

FYI: When I implemented it, it initially looked bad, but I improved that by using the following style in the AndroidManifest for my SettingsActivity:

<style name="Theme.Preferences" parent="@android:style/Theme.Holo.NoActionBar.Fullscreen" />

I had no option to not update to XE16 (other than turning off network connectivity). After the update I tweaked my app's API usages for the few XE16 changes. Everything mostly worked.

The first thing I noticed was that my swiping down in my immersion's MainActivity would no longer go back to the Live Card. I fixed this by having my MainActivity's onGesture handler return false for Gesture.SWIPE_DOWN.

The second thing I noticed is the purpose of this question: My SettingsActivity that wraps a PreferenceFragment no longer allows me to move up and down the preference list using the swipe left and right. My code is at the end of this post. I added a GestureDetector to help debug this problem after it was noticed. I can see the SWIPE_LEFT and SWIPE_RIGHT being logged, but no matter what I return, or even if I remove the gesture code, the preference list selection never moves from the first item. The first item is a CheckBoxPreference, which does toggle when I tap.

I have seen several other Glass apps that use Android Preferences (either PreferenceActivity or PreferenceFragment), and they all also seem to now be broken.

How to properly implement Preferences on Glass, or how to get PreferenceFragment to work?

public class SettingsActivity //
                extends Activity //
                implements GestureDetector.BaseListener
{
    private static final String TAG             = WtcLog.TAG(SettingsActivity.class);

    public static final int     RESULT_SIGN_OUT = RESULT_FIRST_USER + 1;

    private static final String TAG_PREFERENCES = "preferences";

    private GestureDetector     mGestureDetector;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        mGestureDetector = new GestureDetector(this);
        mGestureDetector.setBaseListener(this);

        getFragmentManager() //
        .beginTransaction() //
        .replace(android.R.id.content, new FragmentSettings(), TAG_PREFERENCES) //
        .commit();
    }

    public static class FragmentSettings extends PreferenceFragment
    {
        private ApplicationGlass   mApplication;
        private WavePreferences    mPreferences;

        private PreferenceScreen   mScreenTop;
        private PreferenceCategory mCategoryDebug;

        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);

            Activity activity = getActivity();
            mApplication = (ApplicationGlass) activity.getApplication();
            mPreferences = mApplication.getPreferences();

            addPreferencesFromResource(R.xml.preferences);

            mScreenTop = (PreferenceScreen) findPreference("screen_top");

            //
            // Remember Password
            //
            CheckBoxPreference prefRememberPassword = (CheckBoxPreference) findPreference("pref_remember_password");
            prefRememberPassword.setChecked(mPreferences.getRememberPassword());
            prefRememberPassword.setOnPreferenceChangeListener(new OnPreferenceChangeListener()
            {
                @Override
                public boolean onPreferenceChange(Preference preference, Object newValue)
                {
                    boolean rememberPassword = (Boolean) newValue;

                    String password = null;
                    if (rememberPassword)
                    {
                        password = mApplication.getSessionManager().getLastStartConnectInfo().getPassword();
                    }

                    mPreferences.setRememberPassword(rememberPassword);
                    mPreferences.setPassword(password);

                    return true;
                }
            });

            ...
        }
    }

    @Override
    public boolean onGenericMotionEvent(MotionEvent event)
    {
        if (mGestureDetector != null)
        {
            return mGestureDetector.onMotionEvent(event);
        }
        return false;
    }

    @Override
    public boolean onGesture(Gesture gesture)
    {
        WtcLog.debug(TAG, "onGesture(" + gesture + ")");
        switch (gesture)
        {
            case LONG_PRESS:
                WtcLog.debug(TAG, "onGesture: LONG_PRESS");
                break;
            case TAP:
                WtcLog.debug(TAG, "onGesture: TAP");
                break;
            case TWO_TAP:
                WtcLog.debug(TAG, "onGesture: TWO_TAP");
                break;
            case SWIPE_RIGHT:
                WtcLog.debug(TAG, "onGesture: SWIPE_RIGHT");
                break;
            case SWIPE_LEFT:
                WtcLog.debug(TAG, "onGesture: SWIPE_LEFT");
                break;
            case SWIPE_DOWN:
                WtcLog.debug(TAG, "onGesture: SWIPE_DOWN");
                break;
            case SWIPE_UP:
                WtcLog.debug(TAG, "onGesture: SWIPE_UP");
                break;
            case THREE_LONG_PRESS:
                WtcLog.debug(TAG, "onGesture: THREE_LONG_PRESS");
                break;
            case THREE_TAP:
                WtcLog.debug(TAG, "onGesture: THREE_TAP");
                break;
            case TWO_LONG_PRESS:
                WtcLog.debug(TAG, "onGesture: TWO_LONG_PRESS");
                break;
            case TWO_SWIPE_DOWN:
                WtcLog.debug(TAG, "onGesture: TWO_SWIPE_DOWN");
                break;
            case TWO_SWIPE_LEFT:
                WtcLog.debug(TAG, "onGesture: TWO_SWIPE_LEFT");
                break;
            case TWO_SWIPE_RIGHT:
                WtcLog.debug(TAG, "onGesture: TWO_SWIPE_RIGHT");
                break;
            case TWO_SWIPE_UP:
                WtcLog.debug(TAG, "onGesture: TWO_SWIPE_UP");
                break;
            default:
                WtcLog.error(TAG, "onGesture: unknown gesture \"" + gesture + "\"");
                break;
        }
        return false;
    }
}

回答1:

I think this is related to listViews no longer scrolling correctly in XE16. I answered over here on how to get scrolling back: https://stackoverflow.com/a/23146305/1114876.

Essentially, in your switch/case statement for the gestures, call

myListView.setSelection(myListView.getSelectedItemPosition()+1); to go forward in the list and myListView.setSelection(myListView.getSelectedItemPosition()-1); to go backwards.

edit: Here's some info on how a PreferenceActivity inherits the ListActivity class: https://stackoverflow.com/a/8432096/1114876 You can grab the PreferenceActivity's listview by calling getListView(). Then attach the gesture recognizer.



回答2:

Keypad (dpad) events are still handled correctly by ListView, and PreferenceFragment on XE16, so I came up with a workaround by turning the glass touchpad motion events into keypad motion events.

I added below the implementation of a workaround PreferenceFragment instance that automatically handles the conversion. The full implementation, and usage can be found at http://github.com/ne0fhyk/AR-Glass/blob/master/src/com/ne0fhyklabs/freeflight/fragments/GlassPreferenceFragment.java.

In the implementation below, the glass GestureDetector handles the received gestures by dispatching the matching DPAD key event to the current Window.Callback instance.

The class is structured so that you can just drop it within your project, and change your PreferenceFragment implementation to extend from it instead to have it working again.

/**
 * This class provides the necessary workarounds to make the default preference fragment screen
 * work again on glass post XE16.
 */
public class GlassPreferenceFragment extends PreferenceFragment {

    @Override
    public void onStart(){
        super.onStart();

        final Activity parentActivity = getActivity();
        if(parentActivity != null){
            updateWindowCallback(parentActivity.getWindow());
        }
    }

    @Override
    public void onStop(){
        super.onStop();

        final Activity parentActivity = getActivity();
        if(parentActivity != null){
            restoreWindowCallback(parentActivity.getWindow());
        }
    }

    @Override
    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference){
        if(preference instanceof PreferenceScreen){
            //Update the preference dialog window callback. The new callback is able to detect
            // and handle the glass touchpad gestures.
            final Dialog prefDialog = ((PreferenceScreen) preference).getDialog();
            if(prefDialog != null) {
                updateWindowCallback(prefDialog.getWindow());
            }
        }

        return super.onPreferenceTreeClick(preferenceScreen, preference);
    }

    /**
     * Replace the current window callback with one supporting glass gesture events.
     * @param window
     */
    private void updateWindowCallback(Window window){
        if(window == null) {
            return;
        }

        final Window.Callback originalCb = window.getCallback();
        if(!(originalCb instanceof GlassCallback)) {
            final GlassCallback glassCb = new GlassCallback(window.getContext(), originalCb);
            window.setCallback(glassCb);
        }
    }

    /**
     * Restore the original window callback for this window, if it was updated with a glass
     * window callback.
     * @param window
     */
    private void restoreWindowCallback(Window window){
        if(window == null){
            return;
        }

        final Window.Callback currentCb = window.getCallback();
        if(currentCb instanceof GlassCallback){
            final Window.Callback originalCb = ((GlassCallback)currentCb).getOriginalCallback();
            window.setCallback(originalCb);
        }
    }

    /**
     * Window.Callback implementation able to detect, and handle glass touchpad gestures.
     */
    private static class GlassCallback implements Window.Callback {

        /**
         * Used to detect and handle glass touchpad events.
         */
        private final GestureDetector mGlassDetector;

        /**
         * This handles the motion events not supported by the glass window callback.
         */
        private final Window.Callback mOriginalCb;

        public GlassCallback(Context context, Window.Callback original){
            mOriginalCb = original;

            mGlassDetector = new GestureDetector(context);
            mGlassDetector.setBaseListener(new GestureDetector.BaseListener() {
                @Override
                public boolean onGesture(Gesture gesture) {
                    switch(gesture){
                        case TAP:
                            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
                                    KeyEvent.KEYCODE_DPAD_CENTER));
                            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
                                    KeyEvent.KEYCODE_DPAD_CENTER));
                            return true;

                        case SWIPE_LEFT:
                            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
                                    KeyEvent.KEYCODE_DPAD_UP));
                            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
                                    KeyEvent.KEYCODE_DPAD_UP));
                            return true;

                        case SWIPE_RIGHT:
                            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
                                    KeyEvent.KEYCODE_DPAD_DOWN));
                            dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
                                    KeyEvent.KEYCODE_DPAD_DOWN));
                            return true;
                    }

                    return false;
                }
            });
        }

        /**
         * @return the Window.Callback instance this one replaced.
         */
        public Window.Callback getOriginalCallback(){
            return mOriginalCb;
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            return mOriginalCb.dispatchKeyEvent(event);
        }

        @Override
        public boolean dispatchKeyShortcutEvent(KeyEvent event) {
            return mOriginalCb.dispatchKeyShortcutEvent(event);
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            return mOriginalCb.dispatchTouchEvent(event);
        }

        @Override
        public boolean dispatchTrackballEvent(MotionEvent event) {
            return mOriginalCb.dispatchTrackballEvent(event);
        }

        @Override
        public boolean dispatchGenericMotionEvent(MotionEvent event) {
            return mGlassDetector.onMotionEvent(event) || mOriginalCb.dispatchGenericMotionEvent
                    (event);
        }

        @Override
        public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
            return mOriginalCb.dispatchPopulateAccessibilityEvent(event);
        }

        @Nullable
        @Override
        public View onCreatePanelView(int featureId) {
            return mOriginalCb.onCreatePanelView(featureId);
        }

        @Override
        public boolean onCreatePanelMenu(int featureId, Menu menu) {
            return mOriginalCb.onCreatePanelMenu(featureId, menu);
        }

        @Override
        public boolean onPreparePanel(int featureId, View view, Menu menu) {
            return mOriginalCb.onPreparePanel(featureId, view, menu);
        }

        @Override
        public boolean onMenuOpened(int featureId, Menu menu) {
            return mOriginalCb.onMenuOpened(featureId, menu);
        }

        @Override
        public boolean onMenuItemSelected(int featureId, MenuItem item) {
            return mOriginalCb.onMenuItemSelected(featureId, item);
        }

        @Override
        public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) {
            mOriginalCb.onWindowAttributesChanged(attrs);
        }

        @Override
        public void onContentChanged() {
            mOriginalCb.onContentChanged();
        }

        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            mOriginalCb.onWindowFocusChanged(hasFocus);
        }

        @Override
        public void onAttachedToWindow() {
            mOriginalCb.onAttachedToWindow();
        }

        @Override
        public void onDetachedFromWindow() {
            mOriginalCb.onDetachedFromWindow();
        }

        @Override
        public void onPanelClosed(int featureId, Menu menu) {
            mOriginalCb.onPanelClosed(featureId, menu);
        }

        @Override
        public boolean onSearchRequested() {
            return mOriginalCb.onSearchRequested();
        }

        @Nullable
        @Override
        public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
            return mOriginalCb.onWindowStartingActionMode(callback);
        }

        @Override
        public void onActionModeStarted(ActionMode mode) {
            mOriginalCb.onActionModeStarted(mode);
        }

        @Override
        public void onActionModeFinished(ActionMode mode) {
            mOriginalCb.onActionModeFinished(mode);
        }
    }
}


回答3:

Unfortunately Google's official answer seems to be that ListViews (or any UI object using ListViews) should not be used on Glass. From https://code.google.com/p/google-glass-api/issues/detail?id=484#c6:

Due to the vastly different user interaction model on Glass compared to other Android devices, there are a number of stock widgets that will not be readily usable on Glass. ListView is one of these. Instead of trying to hack around these issues to use a widget that will provide your users with a poor UX, you should be migrating your code to use CardScrollView in the GDK.

ListView is listed as being supported on the LiveCard page, so this is somewhat contradictory: https://developers.google.com/glass/develop/gdk/live-cards

I understand that we don't want to be showing long lists to users in Glass, but IMO there are use cases (mine being real-time transit info - see this comment, also the "ok, glass..." list) when you might have one or two more items than what you can fit on the screen, and it makes sense to vertically scroll, not force a cascade onto another Card.

I've asked for some clarification on best practices for vertical lists on Google Glass here.