欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-02 21:41:58
转载请注明出处: 我们平时开发中总会遇见一些奇葩的需求,为了实现这些需求我们往往绞尽脑汁有时候还茶不思饭不香的,有点夸张了(*^__^*)……我印象最深的一个需求是在一段...

转载请注明出处:
我们平时开发中总会遇见一些奇葩的需求,为了实现这些需求我们往往绞尽脑汁有时候还茶不思饭不香的,有点夸张了(*^__^*)……我印象最深的一个需求是在一段文字中对部分词语进行加粗显示。当时费了不少劲,不过还好,这个问题最终解决了,有兴趣的童靴可以看一下: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);
 }
}


代码很简单,运行效果图如下所示:

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

        运行程序之后我们到sdk的tools文件夹下找到hierarchyviewer,双击即可打开,运行之后截图如下:

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

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

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

上图就是我们当前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)这一行代码就实现了关闭输入框的功能,是不是很轻量级并且集成很方便?(*^__^*) ……
我们运行一下程序,效果如下: 

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

恩,看效果感觉还不错,该控件本身并没有什么技术含量,就是要求对activity的层级结构图比较熟悉,然后清楚事件传递机制,最后可以根据坐标来判断点击位置从而决定是否关闭软键盘。
好了,自定义viewgroup,打造自己通用的关闭软键盘控件到这里就告一段落了,感谢收看……