Android UI设计之<十一>自定义ViewGroup,打造通用的关闭键盘小控件ImeObser

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51598682 我们平时开发中总会遇见一些奇葩的需求,为了实现这些需求我们往往绞尽脑汁有时候还茶不思饭不香的,有点夸张了(^__^)……我印象最深的一个需求是在一段文字中对部分词语进行加粗显示。当时费了不少劲,不过还好,这个问题最终解决了,有兴趣的童靴可以看一下:Android UI设计之<六>使用HTML标签,实现在TextView中对部分文字进行加粗显示。 之前产品那边提了这样的需求:用户输入完信息后要求点击非输入框时要把软键盘隐藏。当时看到这个需求觉得没啥难度也比较实际,于是晕晕乎乎的就实现了,可后来产品那边说了只要有输入框的页面全都要按照这个逻辑来,美其名曰用户体验……当时项目中带有输入框的页面不少,如果每个页面都写一遍逻辑,这就严重违背了《重构,改善既有代码的设计》这本书中的说的事不过三原则(事不过三原则说的是如果同样的逻辑代码如果写过三遍以上,就要考虑重构)。于是当时花了点时间搞了个通用的轻量级的关闭键盘的小控件ImeObserverLayout,也是我们今天要讲的主角。 开始讲解代码之前我们先看一下Activity的层级图,学习一下Activity启动之后在屏幕上的视图结构是怎样的,要想清楚Activity的显示层级视图最方便的方式是借助Google给我们提供的工具hierarchyviewer(该工具位于sdk的tools文件夹下)。hierarchyviewer不仅可以把当前正在运行的APP的界面视图层级显示出来,而且还可以通过视图层级优化我们的布局结构。 为了使用hierarchyviewer工具查看当前APP的层级结构,我们先做个简单测试,定义布局文件activity_mian.xml,代码如下: [html] view plain copy 在CODE上查看代码片派生到我的代码片 <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" >

<TextView  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:layout_gravity="center"  
    android:text="测试层级视图" />  

</FrameLayout> 布局文件非常简单,根节点为FrameLayout,中间嵌套了一个TextView,并让TextView居中显示。然后定义MainActivity,代码如下: [java] view plain copy 在CODE上查看代码片派生到我的代码片 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,代码如下:

[java] view plain copy 在CODE上查看代码片派生到我的代码片 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()方法,我们看一下该方法: [java] view plain copy 在CODE上查看代码片派生到我的代码片 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,如果没有保存就添加。 保存完输入框EditText之后还要考虑清空的问题,避免发生内存泄漏。所以ImeObserverLayout又重写了onDetachedFromWindow()方法,然后调用了clearEditText()方法清空所有的EditText。 [java] view plain copy 在CODE上查看代码片派生到我的代码片 private void clearEditText() {
if(null != mEditTexts) {
mEditTexts.clear();
mEditTexts = null;
}
}
保存了EditText之后就是判断隐藏软键盘的逻辑了,为了得到点击坐标,重写了onInterceptTouchEvent()方法,如下所示: [java] view plain copy 在CODE上查看代码片派生到我的代码片 @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(MotionEvent.ACTION_DOWN == ev.getAction() && shouldHideSoftInput(ev)) {
hideSoftInput();
}
return super.onInterceptTouchEvent(ev);
}
在onInterceptTouchEvent()方法中先对事件做了判断,如果是DOWN事件并且shouldHideSoftInput()返回true就调用hideSoftInput()方法隐藏软键盘,我们看一下shouldHideSoftInput()方法,代码如下: [java] view plain copy 在CODE上查看代码片派生到我的代码片 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。 现在我们自定义的ImeObserverLayout准备就绪,接下来就是需要把ImeObserverLayout添加到DecorView和其子视图LinearLayout之间了,为了更方便的使用此控件,我们需要实现添加的逻辑。 添加逻辑要借助Activity来获取根视图DecorView,所以要把当前Activity传递进来,完整代码如下所示: [java] view plain copy 在CODE上查看代码片派生到我的代码片 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中间。 现在一切就绪,测试一下看看效果吧,修改MainActivity代码如下: [java] view plain copy 在CODE上查看代码片派生到我的代码片 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,打造自己通用的关闭软键盘控件到这里就告一段落了,感谢收看……

转载于:https://my.oschina.net/u/1177694/blog/698611

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值