Home » Android » java – Android Actionbar Tabs and Keyboard Focus

java – Android Actionbar Tabs and Keyboard Focus

Posted by: admin June 15, 2020 Leave a comment

Questions:

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 never get the events to capture again. In another application, opening a Dialog and then closing it, however, would allow my key events to go through. Without doing that I’ve found no way of getting my key events again.

What’s the issue here? I can’t find any way to get key events once I swap tabs, and am curious what’s eating them. This example is pretty short and to the point.

Code

main.xml

<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
    >
    <FrameLayout
      android: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"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
    <view 
      class="com.broken.keyboard.KeyboardTestActivity$MyView"
      android:background="#777777"
      android:focusable="true"
      android:focusableInTouchMode="true"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
    >
        <requestFocus/>
    </view>
</LinearLayout>

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;

public class KeyboardTestActivity extends Activity {

    public static class MyView extends View {
        public void toggleKeyboard()
        { ((InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(0, 0); }

        public MyView(Context context)
        { super(context); }

        public MyView(Context context, AttributeSet attrs)
        { super(context, attrs); }

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


        // FIRST PLACE I TRY, WHERE I WANT TO GET THE PRESSES
        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            Log.i("BDBG", "Key went down in view!");
            return super.onKeyDown(keyCode,event);
        }

        // Toggle keyboard on touch!
        @Override
        public boolean onTouchEvent(MotionEvent event)
        {
            if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN)
            {
                toggleKeyboard();
            }
            return super.onTouchEvent(event);
        }
    }

    // Extremely simple fragment
    public class MyFragment extends Fragment {        
        @Override
        public View onCreateView (LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View v = inflater.inflate(R.layout.my_fragment, container, false);
            return v;
        }
    }

    // Simple tab listener
    public static class MyTabListener implements ActionBar.TabListener
    {
        private FragmentManager mFragmentManager=null;
        private Fragment mFragment=null;
        private String mTag=null;
        public MyTabListener(FragmentManager fragmentManager, Fragment fragment,String tag)
        {
            mFragmentManager=fragmentManager;
            mFragment=fragment;
            mTag=tag;
        }
        @Override
        public void onTabReselected(Tab tab, FragmentTransaction ft) {
            // do nothing
        }

        @Override
        public void onTabSelected(Tab tab, FragmentTransaction ft) {
            mFragmentManager.beginTransaction()
                .replace(R.id.actionbar_content, mFragment, mTag)
                .commit();
        }

        @Override
        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
            mFragmentManager.beginTransaction()
                .remove(mFragment)
                .commit();
        }

    }

    FragmentManager mFragmentManager;
    ActionBar mActionBar;

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

        // Retrieve the fragment manager
        mFragmentManager=getFragmentManager();
        mActionBar=getActionBar();

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

        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

        // Add the tabs
        mActionBar.addTab(mActionBar.newTab()
                .setText("Tab 1")
                .setTabListener(new MyTabListener(getFragmentManager(), new MyFragment(),"Frag1")));

        mActionBar.addTab(mActionBar.newTab()
                .setText("Tab 2")
                .setTabListener(new MyTabListener(getFragmentManager(), new MyFragment(),"Frag2")));


    }

    // OTHER PLACE I TRY, DOESN'T WORK BETTER THAN IN THE VIEW
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.i("BDBG", "Key went down in activity!");
        return super.onKeyDown(keyCode,event);
    }
}
How to&Answers:

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

main.xml

my_fragment.xml