天天看點

流式标簽生成控件

同步釋出于avenwu.net

Fork on github https://github.com/avenwu/support

無圖無真相

完整代碼TagInputLayout.java

思路

* A 基于EditView,Html/Span樣式變換
* B 基于ViewGroup,自定義Layout
           

Span樣式

采用span需要解決如下難點:

  • 同一EditView内樣式混排
  • 向前删除這個tag标簽

要求對html,span相關屬性了如指掌,需要計算每個标簽的位置,删除判斷,複雜度很高。

自定義Layout

自定義天生的優點就是任性,do what ever you like。

  • 定義标簽view
  • 動态添加,删除
  • 樣式定義配置

技術難點在于如何設計,怎麼自定義相關标簽,好處繞過删除的難點,複雜度低,偏技術性。

實作方案

暫且選擇方案B,現在思考一下怎麼設計整個互動實作。

設計思路

  • 标簽項,每一個即是一個Label,包括選中狀态,可用TextView
  • 輸入标簽,輸入還是用EditView,需要和整體融合在一起
  • 增加、删除标簽相當于在容器内addView,removeChild
  • 監聽輸入變化,确定分割符,如回車(Enter),逗号(Comma)觸發标簽生成邏輯,回退鍵(Delete/Back key)觸發删除标簽邏輯
  • 細節處理,點選删除位置變化,選中高光,鍵盤顯隐,自動換行等等
  • 善後,諸如标簽配置化問題

代碼裡看細節

此處省略2萬字。。。。。

現在我看幾個關鍵性代碼。另外相信認真看這篇文章的,絕大多數都是有經驗的工程師,很容易了解。

生成标簽

第一波代碼是标簽項的生成, 每次執行個體化一個TextView,根據我們的需要設定相關屬性,顯示内容從EditView擷取後清空輸入控件,最後将标簽view添加到布局中。

private void generateTag(CharSequence tag) {
CharSequence tagString = tag == null ? mInputView.getText().toString() : tag;
mInputView.getText().clear();
final int targetIndex = indexOfChild(mInputView);
TextView tagLabel;
if (mDecorator.getLayout() != INVALID_VALUE) {
    View view = View.inflate(getContext(), mDecorator.getLayout(), null);
    if (view instanceof TextView) {
        tagLabel = (TextView) view;
    } else {
        throw new IllegalArgumentException("The custom layout for tagLabel label must have TextView as root element");
    }
} else {
    tagLabel = new TextView(getContext());
    tagLabel.setPadding(mDecorator.getPadding()[0], mDecorator.getPadding()[1], mDecorator.getPadding()[2], mDecorator.getPadding()[3]);
    tagLabel.setTextSize(mDecorator.getTextSize());
}
updateCheckStatus(tagLabel, false);
tagLabel.setText(tagString);
tagLabel.setSingleLine();
tagLabel.setGravity(Gravity.CENTER_VERTICAL);
tagLabel.setEllipsize(TextUtils.TruncateAt.END);
if (mDecorator.getMaxLength() != INVALID_VALUE) {
    InputFilter maxLengthFilter = new InputFilter.LengthFilter(mDecorator.getMaxLength());
    tagLabel.setFilters(new InputFilter[]{maxLengthFilter});
}
tagLabel.setClickable(true);
tagLabel.setOnClickListener(this);
addView(tagLabel, targetIndex);
mInputView.requestFocus();
           

}

監聽輸入内容

對内容的判斷在這裡應該是比較重要的東西,同樣非常簡單,首先确定分隔符,這裡預設使用回車,逗号來分割每一個标簽項。

@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
    if (isKeyCodeHit(keyCode)) {
        if (!TextUtils.isEmpty(mInputView.getText().toString())) {
            generateTag(mInputView.getText().toString());
        }
        return true;
    } else if (KeyEvent.KEYCODE_DEL == keyCode) {
        if (TextUtils.isEmpty(mInputView.getText().toString())) {
            deleteTag();
            return true;
        }
    }
}
return false;
}

public void afterTextChanged(Editable s) {
if (s.length() > 0) {
    if (isKeyCharHit(s.charAt(0))) {
        mInputView.setText("");
    } else if (isKeyCharHit(s.charAt(s.length() - 1))) {
        mInputView.setText("");
        generateTag(s.subSequence(0, s.length() - 1) + "");
    }
}
}
           

删除标簽

能加标簽當然也能删除标簽,考慮一下什麼時候删除的是标簽?當輸入框為空的時候我們接着删除實際上急需要删除整個前面一項标簽了。同時通過點選某一項标簽,我們需要能删除目前選中的。

private void deleteTag() {
if (getChildCount() > 1) {
    removeViewAt(mCheckIndex == INVALID_VALUE ? indexOfChild(mInputView) - 1 : mCheckIndex);
    mCheckIndex = INVALID_VALUE;
    mInputView.requestFocus();
}
           

Layout排版

這裡有一個問題和業務邏輯關系不大,但是和互動關系很大的的地方,就是layout中每一項view的位置擺放,理想情況下我們需要能讓每個view自動填充在最後,同時可以自動換行,當删除某一項後,如果前面的空間足夠,還要能夠自動向前靠齊。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTop = getPaddingTop();
int childLeft = getPaddingLeft();
final int count = getChildCount();
for (int i = 0; i < count; i++) {
    View child = getChildAt(i);
    if (nullChildView(child)) continue;
    final int childWidth = child.getMeasuredWidth();
    final int childHeight = child.getMeasuredHeight();
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    if (mCachedPosition.get(i, INVALID_VALUE) != INVALID_VALUE) {
        childTop += lp.topMargin + childHeight + lp.bottomMargin;
        childLeft = getPaddingLeft();
    } else if (childTop == getPaddingTop()) {
        childTop += lp.topMargin;
    }
    childLeft += lp.leftMargin;
    setChildFrame(child, childLeft, childTop, childWidth, childHeight);
    childLeft += childWidth + lp.rightMargin;
}
           

要實作這種排版,方法其實很多,但大緻原理是差不多的。在繪制view的時候我們需要手動計算每個view應該放在哪一行的什麼位置,目前行放不下的時候,另起一行接着放。

小結

思路很重要,因為#@&!(……&&……#%&)%¥#@@%……&((&……¥#@))

寫一個如圖的流式标簽生成控件大概就是就是這樣,不能說很簡單,但是其實也沒那麼難,理清思路和方案還是可以寫出來的。

作者:小文字

出處:http://www.cnblogs.com/avenwu/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.