Android 操作栏选项卡和键盘焦点

2022-01-14 00:00:00 android java android-edittext

我有一个包含两个选项卡的非常简单的活动,我正在尝试在自定义视图中处理键盘输入.这很好用......直到我交换标签.一旦我交换标签,我就再也无法捕捉到这些事件了.然而,在另一个应用程序中,打开一个对话框然后关闭它会允许我的关键事件通过.如果不这样做,我将无法再次获取我的关键事件.

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.

<?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);
    }
}

推荐答案

我已经解决了自己的问题,所以我想我会分享解决方案.如果有一些措辞问题,请在评论中纠正我;我尽量做到准确,但我并不完全是安卓专家.这个答案也应该作为一个很好的例子来说明如何处理一般的换出 ActionBar 选项卡.不管你是否喜欢解决方案代码的设计,它应该是有用的.

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.

以下链接帮助我找出了我的问题:http://code.google.com/p/android/issues/detail?id=2705

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

事实证明,手头有两个重要问题.首先,如果一个 View 既是 android:focusable 又是 android:focusableInTouchMode,那么在蜂窝平板电脑上,人们可能会期望点击它和类似的东西会聚焦它.然而,这不一定是真的.如果该视图恰好也是 android:clickable,那么确实点击会聚焦该视图.如果它不可点击,它不会被触摸聚焦.

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.

如果在视图层次结构完全准备好之前对片段内的视图调用requestFocus()",则视图确实会认为它已聚焦;但是,如果软键盘启动,它实际上不会向该视图发送任何事件!更糟糕的是,如果那个 View 是可点击的,此时点击它并不能解决这个键盘焦点问题,因为 View 认为它确实是焦点,没有什么可做的.但是,如果要聚焦某个其他视图,然后再点击该视图,因为它既可点击又可聚焦,它确实会聚焦并将键盘输入定向到该视图.

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.

鉴于该信息,在切换到选项卡时设置焦点的正确方法是在切换到片段后将可运行对象发布到片段的视图层次结构,然后才调用 requestFocus().在视图层次结构完全准备好之后调用 requestFocus() 将聚焦视图以及直接键盘输入到它,如我们所愿.它不会进入那种奇怪的焦点状态,即视图聚焦但键盘输入不知何故没有指向它,如果在视图层次结构完全准备好之前调用 requestFocus() 就会发生这种情况.

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.

同样重要的是,在片段布局的 XML 中使用requestFocus"标签会过早调用 requestFocus().没有理由在片段的布局中使用该标签.在片段之外,也许.. 但不在片段之内.

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.

在代码中,我在片段顶部添加了一个 EditText,仅用于测试点击焦点更改行为,点击自定义视图也会切换软键盘.交换选项卡时,焦点也应默认为自定义视图.我试图有效地注释代码.

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.

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 {

    /**
     * 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
     *
     */
    public static class ActionBarTabManager 
    {
        public static interface TabChangeListener
        {
            /**
             * Invoked when a new tab is selected.
             * 
             * @param tag The tag of this tab's fragment.
             */
            public abstract void onTabSelected(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.
             */
            public abstract void onTabSelectedPost(String tag);

            /**
             * Invoked when the currently selected tab is reselected.
             * 
             * @param tag The tag of this tab's fragment.
             */
            public abstract void onTabReselected(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.
             */
            public abstract void onTabUnselected(String tag);


        }

        // Variables
        Activity mActivity = null;
        ActionBar mActionBar = null;
        FragmentManager mFragmentManager = null;
        TabChangeListener mListener=null;
        View mContainer = null;
        Runnable mTabSelectedPostRunnable = 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.
         */
        public ActionBarTabManager(Activity activity, int containerId, TabChangeListener listener)
        {
            mActivity = activity;
            if (mActivity == null)
                throw new RuntimeException("ActionBarTabManager requires a valid activity!");

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

            mContainer = activity.findViewById(containerId);

            if (mContainer == null)
                throw new RuntimeException("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
         *
         */
        private class TabSelectedPostRunnable implements Runnable
        {
            String mTag = null;
            public TabSelectedPostRunnable(String tag)
            {
                mTag=tag;
            }
            @Override
            public void run() {
                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
         *
         */
        private class TabListener implements ActionBar.TabListener
        {
            private Fragment mFragment=null;
            private String mTag=null;
            public TabListener(Fragment fragment, String tag)
            {
                mFragment=fragment;
                mTag=tag;
            }
            private boolean post(Runnable runnable)
            {
                return mContainer.post(runnable);
            }
            @Override
            public void onTabReselected(Tab tab, FragmentTransaction ft) {
                // no fragment swapping logic necessary

                if (mListener != null) {
                    mListener.onTabReselected(mTag);
                }

            }
            @Override
            public void onTabSelected(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(new TabSelectedPostRunnable(mTag));
            }

            @Override
            public void onTabUnselected(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.
         */
        public void addTab(String title, Fragment fragment, String tag)
        {
            // The tab listener is crucial here.
            mActionBar.addTab(mActionBar.newTab()
                    .setText(title)
                    .setTabListener(new TabListener(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
     *
     */
    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); }


        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            Log.i("BDBG", "Key (" + keyCode + ") went down in the custom view!");
            return true;
        }

        // 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;
        }
    }

    public class MyTabChangeListener implements ActionBarTabManager.TabChangeListener
    {
        public void onTabReselected(String tag) { }
        public void onTabSelected(String tag) { }
        public void onTabSelectedPost(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)
            {
                throw new RuntimeException("Tab with tag of (""+tag+"") should have the view we're looking for, but doesn't!");
            }
            view.requestFocus();
        }
        public void onTabUnselected(String tag) { }
    }

    // Our tab manager
    ActionBarTabManager mActionBarTabManager = null;

    // Our listener
    MyTabChangeListener mListener = new MyTabChangeListener();

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

        // instantiate our tab manager
        mActionBarTabManager = new ActionBarTabManager(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", new MyFragment(), "Frag1");
        mActionBarTabManager.addTab("Tab 2", new MyFragment(), "Frag2");
    }
}

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">
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />

    <!-- note that view is in lower case here -->
    <view 
        class="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>

相关文章