【问题标题】:Android Actionbar Tabs and Keyboard FocusAndroid 操作栏选项卡和键盘焦点
【发布时间】:2011-12-26 15:02:34
【问题描述】:

问题

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

这里有什么问题?一旦我交换标签,我就找不到任何方法来获取关键事件,并且很好奇是什么在吃它们。这个例子很简短,很中肯。

代码

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

【问题讨论】:

    标签: java android android-edittext


    【解决方案1】:

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

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

    解决方案

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

    此外,当换出片段时,会出现与第一次为活动实例化视图时非常相似的问题。只有在完全准备好视图层次结构后,才需要进行某些更改。

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

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

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

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

    代码

    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 {
    
        /**
         * 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>
    

    【讨论】:

    • 这是金!解释了这么多,谢谢你分享你的发现。它对我正在做的事情有很大帮助 - 一个带有 TextViews 的 ListView,其中一个 TextViews 被 EditText 替换以启用内联编辑。虽然我会说你的问题标语有点误导:)
    • 推荐一个更好的,我会调整的。
    • @devmiles.com 我正在尝试实现一个包含 EditTexts 的 ListView。你能告诉我更多信息吗?
    • 我想你在这里问错人了,我只是补充了答案质量。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 2020-03-22
    • 2014-07-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多