Android UI设计系列之自定义ViewGroup打造通用的关闭键盘小控件ImeObserverLayout(9)

转载请注明出处: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,代码如下:

<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,代码如下:

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,如果没有保存就添加。
保存完输入框EditText之后还要考虑清空的问题,避免发生内存泄漏。所以ImeObserverLayout又重写了onDetachedFromWindow()方法,然后调用了clearEditText()方法清空所有的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。
现在我们自定义的ImeObserverLayout准备就绪,接下来就是需要把ImeObserverLayout添加到DecorView和其子视图LinearLayout之间了,为了更方便的使用此控件,我们需要实现添加的逻辑。
添加逻辑要借助Activity来获取根视图DecorView,所以要把当前Activity传递进来,完整代码如下所示:

 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代码如下:

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,打造自己通用的关闭软键盘控件到这里就告一段落了,感谢收看……

(0)

相关推荐

  • Android UI设计系列之自定义TextView属性实现带下划线的文本框(4)

    在Android开发过程中,如果Android系统自带的属性不能满足我们日常开发的需求,那么就需要我们给系统控件添加额外的属性了.假如有个需求是实现带下划线的文本显示(下划线),如果不使用自定义属性的话实现起来也不太难(起码我认为的实现方式是有许多种的),今天就讲解一下如何使用自定义属性来实现上述带下划线的文本框吧.还好Android中自定义属性不是很复杂,也可以归纳为三步走吧. 老规矩,还是先贴出工程目录吧: 一.添加属性文件 在values文件夹中新建attrs.xml文件,在文件中新建属性

  • Android App开发的自动化测试框架UI Automator使用教程

    Android的自动化测试有很多框架,其中ui automator是google官方提供的黑盒UI相关的自动化测试工具,(GitHub主页:case使用java写,今天实践了一下官方文档中样例程序,其中还是有一些小问题需要总结一下的. 环境准备: 1.JDK(是的,你没看错,基础的android开发环境必备),以及对应的环境变量配置,不会的可以自己百度下下 2.Android Studio(IDE尊崇个人意愿) 3.android SDK以及配置 4.ANT(主要用于build我们的脚本,生成j

  • Android 自动化测试经验分享 UiObejct.getFromParent()的使用方法

    1. UiObejct.getFromParent()的用法:从这个名字就知道,就是从当前对象的父对象中查找想要的子对象,该子对象和当前对象应该是同一层级. 如上图所示:Max inactivity before lock是已知条件,4 minutes是我想动态获取的内容.那应该怎么做呢? 先看看结构:TableLayout(curent index is 2) - TableRow(has several indexes) - TextView(has 2 indexes). 1.先获取Tab

  • Android 自动化测试经验分享 深入UiScrollable

    UiScrollable is a UiCollection and provides support for searching for items in a scrollable user interface (UI) elements. This class can be used with horizontally or vertically scrollable controls. UiScrollable是一个UiCollection(这东西还没搞懂),我们可以使用它,在可滑动的页面

  • Android UI设计系列之自定义ListView仿QQ空间阻尼下拉刷新和渐变菜单栏效果(8)

    好久没有写有关UI的博客了,刚刚翻了一下之前的博客,最近一篇有关UI的博客:Android UI设计系列之自定义Dialog实现各种风格的对话框效果(7) ,实现各种风格效果的对话框,在那篇博客写完后由于公司封闭开发封网以及其它原因致使博客中断至今,中断这么久很是惭愧,后续我会尽量把该写的都补充出来.近来项目有个需求,要做个和QQ空间类似的菜单栏透明度渐变和下拉刷新带有阻尼回弹的效果.于是花点时间动手试了试,基本上达到了QQ空间的效果,截图如下: 通过观察QQ空间的运行效果,发现当往上滚动时菜单

  • Android 中构建快速可靠的 UI 测试

    前言 让我一起来看看 Iván Carballo和他的团队是如何使用Espresso, Mockito 和Dagger 2 编写250个UI测试,并且只花了三分钟就运行成功的. 在这篇文章中,我们会探索如何使用Mockito(译者注:Mockito是java编写的一个单元测试框架),Dagger 2 去创建快速可靠的Android UI测试.如果你正在开始编写Android中的UI 测试或者希望改善已有测试性能的开发者,那么这篇文章值得一读. 我第一次在安卓应用中使用UI自动化测试是在几年前使用

  • AndroidGUI27中findViewById返回null的快速解决办法

    在用Eclipse进行Android的界面开发,通过findViewById试图获取界面元素对象时,该方法有时候返回null,造成这种情况主要有以下两种情形. 第一种情形是最普通的. 比如main.xml如下,其中有一个ListView,其id为lv_contactbook <?xml version="1.0"encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.a

  • Android UI设计系列之自定义EditText实现带清除功能的输入框(3)

    最近公司的产品在陆续做升级,上级领导给的任务是优化代码结构以及项目架构,力争把项目写的精巧简练,于是我们满工程找冗余... 我们都知道每一个项目基本上都是有登陆页的,在登陆页中肯定是少不了输入框了,当我们在输入框中输入数据后如果输入的内容不正确或者是错误的或者是想重新输入,如果嗯键盘上的删除键就得一个一个的去删除,这时候我们或许就想要是能有一个标记当点击了这个标记能把我们刚刚输入的内容清空就好了.这样可以极大的提升用户体验,就拿QQ的登陆来说吧,效果如下: 当点击密码框右侧的小×图标时输入的内容

  • Android UI设计系列之HTML标签实现TextView设置中文字体加粗效果(6)

    搞软件开发的都知道项目中各种需求都有,而有时候各种奇葩的需求真是让人大跌眼镜,为了实现这些奇葩的需求我们往往苦逼的废寝忘食,我现在的项目中就有一个应该算得上奇葩的需求吧,需求是这样的:在一段文字中实现对部分文字加粗 这个看上去也不难并且有点小儿科,因为TextView中有个属性是Android:textStyle,它其中一个值是bold,也就是说是对将要显示的文字进行加粗,于是我就在TextView控件中添加了此属性,代码如下: <TextView android:layout_width=&quo

  • Android UI设计系列之自定义DrawView组件实现数字签名效果(5)

    最近项目中有个新的需求,用户在完交易需要进行输入支付密码付款的时候,要让用户签下自己的签名,提起到数字签名这个东西,感觉有点高大上,后来想想数字签名的原理也不是太复杂,主要实现原理就是利用了View的绘图原理,把用户在屏幕上的手指移动轨迹显示在屏幕上,接着把在屏幕上显示的轨迹View转换成一张图片,最后把图片保存到本地或者上传到服务器... 还是老规矩,首先看一下工程目录吧: public class DrawView extends View { /** * 签名画笔 */ private P

  • Android中使用GridView进行应用程序UI布局的教程

    0.简介 GridView 和 ListView 有共同的父类:AbsListView,因此 GridView 和 ListView 具有一定 的相似性.GridView与ListView的主要区别在于:ListView只是在一个方向上分布:而 GridView则会在两个方向上分布. 与ListView类似的是,GridView也需要通过Adapter来提供显示的数据:开发者既可通 过SimpleAdapter来为GridView提供数据,也可通过开发 BaseAdaptei的子类来为GridV

  • Android UI设计系列之自定义Dialog实现各种风格的对话框效果(7)

    虽然Android给我们提供了众多组件,但是使用起来都不是很方便,我们开发的APK都有自己的风格,如果使用了系统自带的组件,总是觉得和应用的主题不着边际并且看起来也不顺心,那我们就需要自定义了,为了方便大家对自定义组件的学习,我接下来准备了几遍有关自定义的Dialog的文章,希望对大家有帮助. 在开发APK中最常见的估计就数弹出对话框了,这种对话框按照按钮数量来分大致是三种:一个按钮,两个按钮,三个按钮.现在要讲的就是按照按钮数量分为以上三类吧(当然了可以有更多的按钮,只要你愿意). 自定义Di

  • Android使用Handler和Message更新UI

    在Android中,在非主线程中更新UI控件是不安全的,app在运行时会直接Crash,所以当我们需要在非主线程中更新UI控件,那么就需要用到Handler和Message来实现 Demo中,使用到一个按钮和一个TextView,点击按钮之后改变TextView的内容,按钮点击时候新建一个进程,在进程中对UI控件进行修改. public class MainActivity extends Activity implements OnClickListener { private static

随机推荐