Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android UI設計系列之自定義ViewGroup打造通用的關閉鍵盤小控件ImeObserverLayout(9)

Android UI設計系列之自定義ViewGroup打造通用的關閉鍵盤小控件ImeObserverLayout(9)

編輯:關於Android編程

轉載請注明出處: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,打造自己通用的關閉軟鍵盤控件到這裡就告一段落了,感謝收看……

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved