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" >html

<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 { java

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
}

}
代碼很簡單,運行效果圖以下所示:android

運行程序以後咱們到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 {ide

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,打造本身通用的關閉軟鍵盤控件到這裏就告一段落了,感謝收看……
相關文章
相關標籤/搜索