<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent" ><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:text="测试层级视图" /></FrameLayout>布局文件非常简单,根节点为FrameLayout,中间嵌套了一个TextView,并让TextView居中显示。然后定义MainActivity,代码如下:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}代码很简单,运行效果图如下所示:
运行程序之后我们到sdk的tools文件夹下找到hierarchyviewer,双击即可打开,运行之后截图如下:

hierarchyviewer打开之后,该工具会列出当前手机可以进行视图层级展示的所有程序,当前正在运行的程序会在列表中以加粗加黑的形式展示。找到我们的程序,双击打开,如下图所示:

上图就是我们当前MainActivity运行时的布局结构,左下侧就是结构图,右侧分别是缩略图和对应的展示位置图,这里不再对工具的具体使用做讲解,有兴趣的童靴可以自行查阅。根据结构图可以发现,当前Activity的根视图是PhoneWindow类下的DercorView,它包含了一个LinearLayout子视图,而子视图LinearLayout下又包含了三个子视图,一个ViewStub和两个FragmeLayout,第一个视图ViewSub显示状态栏部分,第二个视图FrameLayout中包含一个TextView,这是用来显示标题的,对于第三个视图FrameLayout,其id是content,这就是我们在Activity中调用setContentView()方法为当前Activity设置所显示的View视图的直接父视图。
了解了Activity的层级结构后,可以考虑从层级结构入手实现通用的关闭键盘小控件。我们知道在Android体系中事件是层层传递的,也就是说事件首先传递给根视图DecorView,然后依次往下传递并最终传到目标视图。如果在根视图DecorView和其子视图LinearLayout中间添加一个我们自定义的ViewGroup,那我们就可以在自定义的ViewGroup中对事件进行拦截从而判断是否关闭软键盘。
既然要在DecorView和其子视图LinearLayout中间添加一个自定义的ViewGroup就要首先得到DecorView,从上边Activity的结构图我们知道调用Activity的setContentView()给Activity设置Content时最终都是添加到id为content的FrameLayout下,所以可以根据id得到此FrameLayout,然后依次循环往上找parent,直到找到一个没有parent的View,那这个View就是DecorView。这种方法可行但不是推荐的做法,Google工程师在构造Activity的时候给Activity添加了一个getWindow()方法,该方法返回一个代表窗口的Window对象,该Window类是抽象类,其有一个方法getDecorView(),看过FrameWork源码的童靴应该清楚该方法返回的就是根视图DecorView,所以我们采用这种方式。
现在可以获取到根视图DecorView了,接下来就是考虑我们的ViewGroup应具备的功能了。首先要实现点击输入框EditText之外的区域关闭软键盘就要知道当前布局中有哪些EditText,因此自定义的ViewGroup中要有一个集合,该集合用来保存当前布局文件中的所有的输入框EditText;其次在什么时机查找并保存当前布局中的所有输入框EditText,又在什么时机清空保存的输入框EditText;再次当手指点击屏幕时可以获取到点击的XY坐标,根据点击坐标判断点击位置是否落在输入框EditText中从而决定是否关闭软键盘。
带着以上问题开始实现我们的ViewGroup,代码如下:
public class ImeObserverLayout extends FrameLayout { private List<EditText> mEditTexts;public ImeObserverLayout(Context context) { super(context); }public ImeObserverLayout(Context context, AttributeSet attrs) { super(context, attrs); }public ImeObserverLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }@SuppressLint("NewApi") public ImeObserverLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }@Override protected void onAttachedToWindow() { super.onAttachedToWindow(); collectEditText(this); } @Override protected void onDetachedFromWindow() { clearEditText(); super.onDetachedFromWindow(); }@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(MotionEvent.ACTION_DOWN == ev.getAction() && shouldHideSoftInput(ev)) {hideSoftInput(); } return super.onInterceptTouchEvent(ev); }private void collectEditText(View child) { if(null == mEditTexts) {mEditTexts = new ArrayList<EditText>(); } if(child instanceof ViewGroup) {final ViewGroup parent = (ViewGroup) child;final int childCount = parent.getChildCount();for(int i = 0; i < childCount; i++) {View childView = parent.getChildAt(i);collectEditText(childView);} } else if(child instanceof EditText) {final EditText editText = (EditText) child;if(!mEditTexts.contains(editText)) {mEditTexts.add(editText);} } }private void clearEditText() { if(null != mEditTexts) {mEditTexts.clear();mEditTexts = null; } } private void hideSoftInput() { final Context context = getContext().getApplicationContext(); InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(getWindowToken(), 0); } private boolean shouldHideSoftInput(MotionEvent ev) { if(null == mEditTexts || mEditTexts.isEmpty()) {return false; } final int x = (int) ev.getX(); final int y = (int) ev.getY(); Rect r = new Rect(); for(EditText editText : mEditTexts) {editText.getGlobalVisibleRect(r);if(r.contains(x, y)) {return false;} } return true; }} ImeObserverLayout继承了FrameLayout并定义了属性mEditTexts,mEditTexts用来保存当前页面中的所有输入框EditText。查找所有输入框EditText的时机我们选定了onAttachedToWindow()方法,当该View被添加到窗口上后次方法会被调用,所以ImeObserverLayout重写了onAttachedToWindow()方法并在该方法中调用了collectEditText()方法,我们看一下该方法:private void collectEditText(View child) { if(null == mEditTexts) { mEditTexts = new ArrayList<EditText>(); } if(child instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) child; final int childCount = parent.getChildCount(); for(int i = 0; i < childCount; i++) {View childView = parent.getChildAt(i);collectEditText(childView); } } else if(child instanceof EditText) { final EditText editText = (EditText) child; if(!mEditTexts.contains(editText)) {mEditTexts.add(editText); } }}collectEditText()方法首先对mEditTexts做了非空校验,接着判断传递进来的View是否是ViewGroup类型,如果是ViewGroup类型就循环其每一个子View并递归调用collectEditText()方法;如果传递进来的是EditText类型,就判断当前集合中是否已经保存了该EditText,如果没有保存就添加。private void clearEditText() { if(null != mEditTexts) { mEditTexts.clear(); mEditTexts = null; }}保存了EditText之后就是判断隐藏软键盘的逻辑了,为了得到点击坐标,重写了onInterceptTouchEvent()方法,如下所示: private void clearEditText() { if(null != mEditTexts) { mEditTexts.clear(); mEditTexts = null; }} 在onInterceptTouchEvent()方法中先对事件做了判断,如果是DOWN事件并且shouldHideSoftInput()返回true就调用hideSoftInput()方法隐藏软键盘,我们看一下shouldHideSoftInput()方法,代码如下:private boolean shouldHideSoftInput(MotionEvent ev) { if(null == mEditTexts || mEditTexts.isEmpty()) { return false; } final int x = (int) ev.getX(); final int y = (int) ev.getY(); Rect r = new Rect(); for(EditText editText : mEditTexts) { editText.getGlobalVisibleRect(r); if(r.contains(x, y)) {return false; } } return true;}shouldHideSoftInput()方法首先判断mEditTexts是否为null或者是否保存有EditText,如果为null或者是空的直接返回false就表示不需要关闭软键盘,否则循环遍历所有的EditText,根据点击的XY坐标判断点击位置是否在EditText区域内,如果点击坐标在EditText的区域内直接返回false,否则返回true。 public final class ImeObserver {private ImeObserver() { }public static void observer(final Activity activity) { if (null == activity) {return; } final View root = activity.getWindow().getDecorView(); if (root instanceof ViewGroup) {final ViewGroup decorView = (ViewGroup) root;if (decorView.getChildCount() > 0) {final View child = decorView.getChildAt(0);decorView.removeAllViews();LayoutParams params = child.getLayoutParams();ImeObserverLayout observerLayout = new ImeObserverLayout(activity.getApplicationContext());observerLayout.addView(child, params);LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);decorView.addView(observerLayout, lp);} } }private static class ImeObserverLayout extends FrameLayout { private List<EditText> mEditTexts; public ImeObserverLayout(Context context) {super(context); } public ImeObserverLayout(Context context, AttributeSet attrs) {super(context, attrs); } public ImeObserverLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr); } @SuppressLint("NewApi") public ImeObserverLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes); } @Override protected void onAttachedToWindow() {super.onAttachedToWindow();collectEditText(this); } @Override protected void onDetachedFromWindow() {clearEditText();super.onDetachedFromWindow(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) {if (MotionEvent.ACTION_DOWN == ev.getAction() && shouldHideSoftInput(ev)) {hideSoftInput();}return super.onInterceptTouchEvent(ev); } private void collectEditText(View child) {if (null == mEditTexts) {mEditTexts = new ArrayList<EditText>();}if (child instanceof ViewGroup) {final ViewGroup parent = (ViewGroup) child;final int childCount = parent.getChildCount();for (int i = 0; i < childCount; i++) { View childView = parent.getChildAt(i); collectEditText(childView);}} else if (child instanceof EditText) {final EditText editText = (EditText) child;if (!mEditTexts.contains(editText)) { mEditTexts.add(editText);}} } private void clearEditText() {if (null != mEditTexts) {mEditTexts.clear();mEditTexts = null;} } private void hideSoftInput() {final Context context = getContext().getApplicationContext();InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);imm.hideSoftInputFromWindow(getWindowToken(), 0); } private boolean shouldHideSoftInput(MotionEvent ev) {if (null == mEditTexts || mEditTexts.isEmpty()) {return false;}final int x = (int) ev.getX();final int y = (int) ev.getY();Rect r = new Rect();for (EditText editText : mEditTexts) {editText.getGlobalVisibleRect(r);if (r.contains(x, y)) { return false;}}return true; } }}我们把ImeObserverLayout以内部静态类的方式放入了ImeObserver中,并设置了ImeObserverLayout为private的,目的就是不让外界对其做操作等,然后给ImeObserver添加了一个静态方法observer(Activity activity),在该方法中把ImeObserverLayout添加进了根视图DecorView和其子视图LinearLayout中间。public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_ime); ImeObserver.observer(this); }}MainActivity的代码不需要改动,只是在setContentView()方法后添加了ImeObserver.observer(this)这一行代码就实现了关闭输入框的功能,是不是很轻量级并且集成很方便?(*^__^*) ……
恩,看效果感觉还不错,该控件本身并没有什么技术含量,就是要求对Activity的层级结构图比较熟悉,然后清楚事件传递机制,最后可以根据坐标来判断点击位置从而决定是否关闭软键盘。
好了,自定义ViewGroup,打造自己通用的关闭软键盘控件到这里就告一段落了,感谢收看……