Skip to content Skip to sidebar Skip to footer

Android Actionbar Tabs And Keyboard Focus

Problem I have a very simple activity with two tabs, and I'm trying to handle keyboard input in a custom view. This works great... until I swap tabs. Once I swap tabs, I can neve

Solution 1:

I've solved my own problem, so I thought I'd share the solution. If there's some wording issue, please correct me in a comment; I'm trying to be as accurate as I can but I'm not entirely an android expert. This answer should also serve as an excellent example of how to handle swapping out ActionBar tabs in general. Whether or not one likes the design of the solution code, it should be useful.

The following link helped me figure out my issue: http://code.google.com/p/android/issues/detail?id=2705

Solution

It turns out, there are two important issues at hand. Firstly, if a View is both android:focusable and android:focusableInTouchMode, then on a honeycomb tablet one might expect that tapping it and similar would focus it. This, however, is not necessarily true. If that View happens to also be android:clickable, then indeed tapping will focus the view. If it is not clickable, it will not be focused by touch.

Furthermore, when swapping out a fragment there's an issue very similar to when first instantiating the view for an activity. Certain changes need to be made only after the View hierarchy is completely prepared.

If you call "requestFocus()" on a view within a fragment before the View hierarchy is completely prepared, the View will indeed think that it is focused; however, if the soft keyboard is up, it will not actually send any events to that view! Even worse, if that View is clickable, tapping it at this point will not fix this keyboard focus issue, as the View thinks that it is indeed focused and there is nothing to do. If one was to focus some other view, and then tap back onto this one, however, as it is both clickable and focusable it would indeed focus and also direct keyboard input to this view.

Given that information, the correct approach to setting the focus upon swapping to a tab is to post a runnable to the View hierarchy for the fragment after it is swapped in, and only then call requestFocus(). Calling requestFocus() after the View hierarchy is fully prepared will both focus the View as well as direct keyboard input to it, as we want. It will not get into that strange focused state where the view is focused but the keyboard input is somehow not directed to it, as will happen if calling requestFocus() prior to the View hierarchy being fully prepared.

Also important, using the "requestFocus" tag within the XML of a fragment's layout will most call requestFocus() too early. There is no reason to ever use that tag in a fragment's layout. Outside of a fragment, maybe.. but not within.

In the code, I've added an EditText to the top of the fragment just for testing tap focus change behaviors, and tapping the custom View will also toggle the soft keyboard. When swapping tabs, the focus should also default to the custom view. I tried to comment the code effectively.

Code

KeyboardTestActivity.java

package com.broken.keyboard;

import android.app.ActionBar;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;

import android.app.FragmentTransaction;
import android.app.ActionBar.Tab;
import android.content.Context;

publicclassKeyboardTestActivityextendsActivity {

    /**
     * This class wraps the addition of tabs to the ActionBar,
     * while properly swapping between them.  Furthermore, it
     * also provides a listener interface by which you can
     * react additionally to the tab changes.  Lastly, it also
     * provides a callback for after a tab has been changed and
     * a runnable has been post to the View hierarchy, ensuring
     * the fragment transactions have completed.  This allows
     * proper timing of a call to requestFocus(), and other
     * similar methods.
     * 
     * @author nacitar sevaht
     *
     */publicstaticclassActionBarTabManager 
    {
        publicstaticinterfaceTabChangeListener
        {
            /**
             * Invoked when a new tab is selected.
             * 
             * @param tag The tag of this tab's fragment.
             */publicabstractvoidonTabSelected(String tag);

            /**
             * Invoked when a new tab is selected, but after
             * a Runnable has been executed after being post
             * to the view hierarchy, ensuring the fragment
             * transaction is complete.
             * 
             * @param tag The tag of this tab's fragment.
             */publicabstractvoidonTabSelectedPost(String tag);

            /**
             * Invoked when the currently selected tab is reselected.
             * 
             * @param tag The tag of this tab's fragment.
             */publicabstractvoidonTabReselected(String tag);

            /**
             * Invoked when a new tab is selected, prior to {@link onTabSelected}
             * notifying that the previously selected tab (if any) that it is no
             * longer selected.
             * 
             * @param tag The tag of this tab's fragment.
             */publicabstractvoidonTabUnselected(String tag);


        }

        // VariablesActivitymActivity=null;
        ActionBarmActionBar=null;
        FragmentManagermFragmentManager=null;
        TabChangeListener mListener=null;
        ViewmContainer=null;
        RunnablemTabSelectedPostRunnable=null;

        /**
         * The constructor of this class.
         * 
         * @param activity The activity on which we will be placing the actionbar tabs.
         * @param containerId The layout id of the container, preferable a  {@link FrameLayout}
         *        that will contain the fragments.
         * @param listener A listener with which one can react to tab change events.
         */publicActionBarTabManager(Activity activity, int containerId, TabChangeListener listener)
        {
            mActivity = activity;
            if (mActivity == null)
                thrownewRuntimeException("ActionBarTabManager requires a valid activity!");

            mActionBar = mActivity.getActionBar();
            if (mActionBar == null)
                thrownewRuntimeException("ActionBarTabManager requires an activity with an ActionBar.");

            mContainer = activity.findViewById(containerId);

            if (mContainer == null)
                thrownewRuntimeException("ActionBarTabManager requires a valid container (FrameLayout, preferably).");

            mListener = listener;
            mFragmentManager = mActivity.getFragmentManager();

            // Force tab navigation mode
            mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
        }

        /**
         * Simple Runnable to invoke the {@link onTabSelectedPost} method of the listener.
         * 
         * @author nacitar sevaht
         *
         */privateclassTabSelectedPostRunnableimplementsRunnable
        {
            StringmTag=null;
            publicTabSelectedPostRunnable(String tag)
            {
                mTag=tag;
            }
            @Overridepublicvoidrun() {
                if (mListener != null) {
                    mListener.onTabSelectedPost(mTag);
                }
            }

        }

        /**
         * Internal TabListener.  This class serves as a good example
         * of how to properly handles swapping the tabs out.  It also
         * invokes the user's listener after swapping.
         * 
         * @author nacitar sevaht
         *
         */privateclassTabListenerimplementsActionBar.TabListener
        {
            private Fragment mFragment=null;
            private String mTag=null;
            publicTabListener(Fragment fragment, String tag)
            {
                mFragment=fragment;
                mTag=tag;
            }
            privatebooleanpost(Runnable runnable)
            {
                return mContainer.post(runnable);
            }
            @OverridepublicvoidonTabReselected(Tab tab, FragmentTransaction ft) {
                // no fragment swapping logic necessaryif (mListener != null) {
                    mListener.onTabReselected(mTag);
                }

            }
            @OverridepublicvoidonTabSelected(Tab tab, FragmentTransaction ft) {
                mFragmentManager.beginTransaction()
                    .replace(mContainer.getId(), mFragment, mTag)
                    .commit();
                if (mListener != null) {
                    mListener.onTabSelected(mTag);
                }
                // Post a runnable for this tab
                post(newTabSelectedPostRunnable(mTag));
            }

            @OverridepublicvoidonTabUnselected(Tab tab, FragmentTransaction ft) {
                mFragmentManager.beginTransaction()
                    .remove(mFragment)
                    .commit();
                if (mListener != null) {
                    mListener.onTabUnselected(mTag);
                }
            }

        }

        /**
         * Simple wrapper for adding a text-only tab.  More robust
         * approaches could be added.
         * 
         * @param title The text to display on the tab.
         * @param fragment The fragment to swap in when this tab is selected.
         * @param tag The unique tag for this tab.
         */publicvoidaddTab(String title, Fragment fragment, String tag)
        {
            // The tab listener is crucial here.
            mActionBar.addTab(mActionBar.newTab()
                    .setText(title)
                    .setTabListener(newTabListener(fragment, tag)));   
        }

    }
    /**
     * A simple custom view that toggles the on screen keyboard when touched,
     * and also prints a log message whenever a key event is received.
     * 
     * @author nacitar sevaht
     *
     */publicstaticclassMyViewextendsView {
        publicvoidtoggleKeyboard()
        { ((InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(0, 0); }

        publicMyView(Context context)
        { super(context); }

        publicMyView(Context context, AttributeSet attrs)
        { super(context, attrs); }

        publicMyView(Context context, AttributeSet attrs, int defStyle)
        { super(context, attrs, defStyle); }


        @OverridepublicbooleanonKeyDown(int keyCode, KeyEvent event) {
            Log.i("BDBG", "Key (" + keyCode + ") went down in the custom view!");
            returntrue;
        }

        // Toggle keyboard on touch!@OverridepublicbooleanonTouchEvent(MotionEvent event)
        {
            if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN)
            {
                toggleKeyboard();
            }
            returnsuper.onTouchEvent(event);
        }
    }

    // Extremely simple fragmentpublicclassMyFragmentextendsFragment {
        @Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            Viewv= inflater.inflate(R.layout.my_fragment, container, false);
            return v;
        }
    }

    publicclassMyTabChangeListenerimplementsActionBarTabManager.TabChangeListener
    {
        publicvoidonTabReselected(String tag) { }
        publicvoidonTabSelected(String tag) { }
        publicvoidonTabSelectedPost(String tag)
        {
            // TODO:NOTE: typically, one would conditionally set the focus based upon the tag.//             but in our sample, both tabs have the same fragment layout.
            View view=findViewById(R.id.myview);
            if (view == null)
            {
                thrownewRuntimeException("Tab with tag of (\""+tag+"\") should have the view we're looking for, but doesn't!");
            }
            view.requestFocus();
        }
        publicvoidonTabUnselected(String tag) { }
    }

    // Our tab managerActionBarTabManagermActionBarTabManager=null;

    // Our listenerMyTabChangeListenermListener=newMyTabChangeListener();

    // Called when the activity is first created.@OverridepublicvoidonCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // instantiate our tab manager
        mActionBarTabManager = newActionBarTabManager(this,R.id.actionbar_content,mListener);

        // remove the activity title to make space for tabs
        getActionBar().setDisplayShowTitleEnabled(false);

        // Add the tabs
        mActionBarTabManager.addTab("Tab 1", newMyFragment(), "Frag1");
        mActionBarTabManager.addTab("Tab 2", newMyFragment(), "Frag2");
    }
}

main.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="fill_parent"android:layout_height="fill_parent"
    ><FrameLayoutandroid:id="@+id/actionbar_content"android:layout_width="match_parent"android:layout_height="match_parent"
    /></LinearLayout>

my_fragment.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><EditTextandroid:layout_width="fill_parent"android:layout_height="wrap_content"
        /><!-- note that view is in lower case here --><viewclass="com.broken.keyboard.KeyboardTestActivity$MyView"android:id="@+id/myview"android:background="#777777"android:clickable="true"android:focusable="true"android:focusableInTouchMode="true"android:layout_width="fill_parent"android:layout_height="match_parent"
    /></LinearLayout>

Post a Comment for "Android Actionbar Tabs And Keyboard Focus"