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

转载请注明出处: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()方法,如下所示:

@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()方法,代码如下:

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

时间: 2024-10-22 19:37:30

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

Android UI设计之&lt;十&gt;自定义ListView,实现QQ空间阻尼下拉刷新和渐变菜单栏效果

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51559694 好久没有写有关UI的博客了,刚刚翻了一下之前的博客,最近一篇有关UI的博客是在2014年写的:Android UI设计之<七>自定义Dialog,实现各种风格效果的对话框,在那篇博客写完后由于公司封闭开发封网以及其它原因致使博客中断至今,中断这么久很是惭愧,后续我会尽量把该写的都补充出来.近来项目有个需求,要做个和QQ空间类似的菜单栏透明度渐变和下拉刷新带有阻尼回弹的效

【转】【Android UI设计与开发】第07期:底部菜单栏(二)Fragment的详细介绍和使用方法

原始地址:http://blog.csdn.net/yangyu20121224/article/category/1431917/1 由于TabActivity在Android4.0以后已经被完全弃用,那么我就不再浪费口水继续讲解它了,取而代之的是Fragment.Fragment是Android3.0新增的概念,Fragment翻译成中文是碎片的意思,不过却和Activity十分的相似,这一篇我花大量的篇幅来详细的讲解Fragment的介绍和使用方法. 一.Fragment的基础知识介绍  

【Android UI设计】Dialog对话框详解(二)

上一篇我们介绍了Dialog的基本使用方法,[Android UI设计]Dialog对话框详解(一)今天继续介绍,废话不多说,今天主要实现ProgressDialog和透明Dialog两种效果,最后介绍一下github上的一个Dialog动画开源库,里面包含多种动画特效,效果图如下: 一.ProgressDialog基本使用 1.ProgressDialog关键代码 mProgressDialog = new ProgressDialog(MainActivity.this); // 圆形pro

【Android UI设计与开发】第05期:引导界面(五)实现应用程序只启动一次引导界面

[Android UI设计与开发]第05期:引导界面(五)实现应用程序只启动一次引导界面 jingqing 发表于 2013-7-11 14:42:02 浏览(229501) 这篇文章算是对整个引导界面开发专题的一个终结了吧,个人觉得大部分的引导界面基本上都是千篇一律的,只要熟练掌握了一个,基本上也就没什么好说的了,要是在今后的开发中遇到了更好玩,更有趣的引导界面,博主也会在这里及时的跟大家分享,今天的内容主要是教大家的应用程序只有在第一次启动的时候显示引导界面,以后在启动程序的时候就不再显示了

android UI设计时需要注意遵循的设计原则

1.Android设备屏幕尺寸分布 首先看一下各种屏幕的尺寸和屏幕密度划分,下图是各种屏幕尺寸对应的范围: 从上图可以看出,对应normal尺寸的屏幕范围集中在常见的3到5寸屏之间,large尺寸对应的就主要是5到7寸的nottpad之类的设备,例如三星的Note和Nexus7平板等,再网上走就是平板电脑了.接下来是屏幕密度(dpi),需要说明的时,平时所说的屏幕分辨率其实不能作为屏幕适配的依据,应该依据屏幕密度和屏幕尺寸来换算,屏幕密度是指每寸屏幕内容纳的像素数,屏幕密度从ldpi到xhdpi

Android UI设计的基本元素有哪些

在android app开发如火如荼的今天,如何让自己的App受人欢迎.如何增加app的下载量和使用量....成为很多android应用开发前,必须讨论的问题.而ui设计则是提升客户视觉体验度.提升下载量和使用量等等的一个比较关键的因素.今天小编整了了一些android  ui设计的基本元素,供android初学者学习借鉴. 移动端的App界面,不论是iOS还是Android ,一般都由四个元素组成,分别是:状态栏(status bar).导航栏(navigation).主菜单栏(submenu

Android UI设计规则

Android UI技巧 1.1 不该做什么 l  不要照搬你在其它平台的UI设计,应该让用户使用感觉是在真正使用一个Android软件,在你的LOGO显示和平台整体观感之间做好平衡 l  不要过度使用模态对话框 l  不要使用px单位,使用dp或者为文本使用sp l  不要使用固定的绝对定位的布局 l  不要使用太小的字体 1.2 该做什么 l  要为高分辨率的屏幕创建资源 l  要使用适当的间距 l  要正确管理活动(Activity) l  要正确处理屏幕的方向变化 l  需要点击的元素要

如何在Android实现桌面清理内存简单Widget小控件

如何在Android实现桌面清理内存简单Widget小控件 我们经常会看到类似于360.金山手机卫士一类的软件会带一个widget小控件,显示在桌面上,上面会显示现有内存大小,然后会带一个按键功能来一键清理内存,杀死后台进程的功能,那么这个功能是如何实现的呢,我们今天也来尝试做一个类似的功能的小控件. 效果图: 一.UI部分的编写: 参照Google的文档,首先在建立一个类继承AppWidgetProvider import android.appwidget.AppWidgetProvider

android中常用的小控件------Widgets的使用

好久没有写博客了,都不知博客怎么写了,最近突然想写博客,以帮助更多的人,却又不知道写什么好呢? 好吧  我承认我有点懒惰了,可是程序猿是不应该懒惰的哦,俺要做个好孩子. 好了言归正传,开始介绍下今天的主要内容吧! Widgets一个桌面的小控件    个人认为是很常用的,不知道大神们是不是这么觉得的呢?比如说你开发的一款音乐播放器的软件,可把基本的上一曲和下一曲.暂停的几个功能放在这个小控件里面将它显示在桌面上来,这样就很方便啦,你想要下一曲.上一曲.暂停播放的时候,就不用再打开播放器了,而是直