Android操作栏标签和键盘焦点

时间:2011-11-10 23:12:20

标签: java android android-edittext

问题

我有一个非常简单的活动,有两个标签,我正在尝试在自定义视图中处理键盘输入。这很好用......直到我交换标签。一旦我交换标签,我就永远无法再次捕获事件。在另一个应用程序中,打开一个Dialog然后关闭它会允许我的关键事件通过。没有这样做,我发现无法重新获得关键事件。

这里有什么问题?一旦我换掉标签,我找不到任何方法来获取关键事件,我很好奇吃了什么。这个例子非常简短而且非常重要。

代码

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

1 个答案:

答案 0 :(得分:11)

我已经解决了自己的问题,所以我想我会分享解决方案。如果有一些措辞问题,请在评论中纠正我;我想尽可能准确但我不完全是一个Android专家。这个答案也应该成为如何处理一般的ActionBar选项卡交换的一个很好的例子。无论是否喜欢解决方案代码的设计,它都应该是有用的。

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

解决方案

事实证明,手头有两个重要问题。首先,如果一个视图同时是android:focusable和android:focusableInTouchMode,那么在蜂窝平板电脑上可能会有人认为点击它会类似于它。然而,这不一定是真的。如果该视图碰巧是android:clickable,那么确实点击将会聚焦视图。如果它不可点击,则不会通过触摸进行聚焦。

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

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

鉴于该信息,在交换选项卡时设置焦点的正确方法是在交换后将可运行的片段发布到片段的View层次结构,然后再调用requestFocus()。在视图层次结构完全准备好之后调用requestFocus()将完全准备View,以及直接键盘输入,如我们所愿。它不会进入视图聚焦的奇怪的聚焦状态,但键盘输入在某种程度上不是指向它,如果在View层次结构完全准备之前调用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>