Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 開源項目ExpandableTextView使用和源碼分析

開源項目ExpandableTextView使用和源碼分析

編輯:關於Android編程

ExpandableTextView,可展開和收起的TextView,就像GooglePlay裡面顯示應用的描述那樣。

項目地址:
https://github.com/Manabu-GT/ExpandableTextView
其中包含項目源碼和示例代碼。

運行效果圖:
\

在國內很多應用中,也可以看到這種效果的使用場景。如豌豆莢的詳情頁:

\

一、項目使用

(1).在工程的build.gradle文件中添加項目引用。

該項目較簡單,java代碼部分只有ExpandableTextView.java一個文件。

 

dependencies {
    compile 'com.ms-square:expandableTextView:0.1.4'
}
(2).在布局中添加如下代碼。
注意,TextView的id只能定義為expandable_text,ImageButton的id只能定義為expand_collapse。

    
    
ExpandableTextView支持使用自定義屬性。對應的attrs.xml文件及屬性釋義如下。


    
        
        
        
        
        
        
        
        
        
        
    
(3).添加Java代碼。
ExpandableTextView expandableTextView = (ExpandableTextView) rootView.findViewById(R.id.expand_text_view);
// 設置顯示內容
expandableTextView.setText("");
// 設置狀態監聽
expandableTextView.setOnExpandStateChangeListener(new ExpandableTextView.OnExpandStateChangeListener() {
    @Override
    public void onExpandStateChanged(TextView textView, boolean isExpanded) {
    }
});

二、源碼分析

實現原理:
該View擴展自LinearLayout,由一個TextView和一個ImageButton組成,垂直排列。初始時,限定TextView的maxHeight使內容未完全顯示,此時處於“收起”狀態。當被點擊時,為View添加自定義動畫,在動畫執行過程中,不斷的修改TextView的maxHeight和整個View的height,達到“展開”和“收起”的效果。

下面進入代碼部分。其核心在於自定義動畫的實現,和onMeasure()、onClick()方法。

(1).類的定義。

 

ExpandableTextView繼承自LinearLayout類,實現了OnClickListener接口,監聽自身的點擊事件,處理“展開”和“收起”。

public class ExpandableTextView extends LinearLayout implements View.OnClickListener {
}

(2).成員變量介紹。

 

// 默認行數
private static final int MAX_COLLAPSED_LINES = 8;

// 默認動畫時長
private static final int DEFAULT_ANIM_DURATION = 300;

// 動畫啟動時TextView的默認透明度
private static final float DEFAULT_ANIM_ALPHA_START = 0.7f;

// 顯示內容的TextView
protected TextView mTv;

// 箭頭按鈕ImageButton
protected ImageButton mButton;

// 是否需要重新布局。當調用了setText()方法後,該值置為true
private boolean mRelayout;

// 默認TextView處於收起狀態
private boolean mCollapsed = true;

// 收起狀態下的整個View的高度
private int mCollapsedHeight;

// TextView整個文本的高度
private int mTextHeightWithMaxLines;

// 收起狀態下的最大顯示行數
private int mMaxCollapsedLines;

// TextView的bottomMargin
private int mMarginBetweenTxtAndBottom;

// 箭頭按鈕的展開圖標
private Drawable mExpandDrawable;

// 箭頭按鈕的收起圖標
private Drawable mCollapseDrawable;

// 動畫執行時長
private int mAnimationDuration;

// 動畫啟動時顯示內容的透明度
private float mAnimAlphaStart;

// 是否正在執行動畫
private boolean mAnimating;

// 狀態改變的監聽
private OnExpandStateChangeListener mListener;

// 如果是在ListView中,需要使用到mCollapsedStatus和mPosition,保存當前position的展開或收起狀態
private SparseBooleanArray mCollapsedStatus;
private int mPosition;

(3).構造方法。
在構造方法中,調用init()方法,執行初始化操作。包括獲取布局文件中的自定義屬性,設置默認按鈕圖標,設置方向,限定布局的方向只能為垂直。

 

public ExpandableTextView(Context context) {
    this(context, null);
}

public ExpandableTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(attrs);
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(attrs);
}

private void init(AttributeSet attrs) {
    TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableTextView);
    mMaxCollapsedLines = typedArray.getInt(R.styleable.ExpandableTextView_maxCollapsedLines, MAX_COLLAPSED_LINES);
    mAnimationDuration = typedArray.getInt(R.styleable.ExpandableTextView_animDuration, DEFAULT_ANIM_DURATION);
    mAnimAlphaStart = typedArray.getFloat(R.styleable.ExpandableTextView_animAlphaStart, DEFAULT_ANIM_ALPHA_START);
    mExpandDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_expandDrawable);
    mCollapseDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_collapseDrawable);

    if (mExpandDrawable == null) {
        mExpandDrawable = getDrawable(getContext(), R.drawable.ic_expand_more_black_12dp);
    }
    if (mCollapseDrawable == null) {
        mCollapseDrawable = getDrawable(getContext(), R.drawable.ic_expand_less_black_12dp);
    }

    typedArray.recycle();

    // 強制把方向設置為垂直
    setOrientation(LinearLayout.VERTICAL);

    // 默認不顯示
    setVisibility(GONE);
}

(4).onFinishInflate()方法。

 

當xml布局文件加載完成之後,後執行onFinishInflate()方法,在這裡完成View的初始化。

@Override
protected void onFinishInflate() {
    findViews();
}

private void findViews() {
    mTv = (TextView) findViewById(R.id.expandable_text);
    mTv.setOnClickListener(this);
    mButton = (ImageButton) findViewById(R.id.expand_collapse);
    mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable);
    mButton.setOnClickListener(this);
}

(5).核心之onMeasure()方法。
首先是條件判斷,當View沒有設置內容或內容為空,或者內容本身較短無需處理展開和收起,此時隱藏箭頭按鈕,直接return代碼不再往下執行。
然後,通過setMaxLines()縮小TextView的顯示行數,計算TextView顯示全部內容時的高度、整個LinearLayout的高度、TextView的bottomMargin值,並保存起來再之後執行動畫時使用。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 如果沒有改變顯示內容,或者顯示內容為空,執行super.onMeasure()並返回
    if (!mRelayout || getVisibility() == View.GONE) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        return;
    }
    mRelayout = false;

    // 先隱藏箭頭按鈕,將文字最大顯示行數設置到最大,後面再根據測量情況修改
    mButton.setVisibility(View.GONE);
    mTv.setMaxLines(Integer.MAX_VALUE);

    // 測量
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 如果內容未超出限定的最大顯示行數,直接返回。在這種情況下,無需展開和收起。
    if (mTv.getLineCount() <= mMaxCollapsedLines) {
        return;
    }

    // 計算TextView的真實高度並保存
    mTextHeightWithMaxLines = getRealTextViewHeight(mTv);

    // 限定TextView的最大行數,並將箭頭按鈕顯示出來
    if (mCollapsed) {
        mTv.setMaxLines(mMaxCollapsedLines);
    }
    mButton.setVisibility(View.VISIBLE);

    // 再次重新測量
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if (mCollapsed) {
        mTv.post(new Runnable() {
            @Override
            public void run() {
                // TextView的bottomMargin
                mMarginBetweenTxtAndBottom = getHeight() - mTv.getHeight();
            }
        });
        // 保存收起狀態下的整個View的高度
        mCollapsedHeight = getMeasuredHeight();
    }
}

// 獲取TextView的真實高度,即內容展開時的高度
private static int getRealTextViewHeight(@NonNull TextView textView) {
    int textHeight = textView.getLayout().getLineTop(textView.getLineCount());
    int padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom();
    return textHeight + padding;
}

(6).核心之自定義動畫的實現。
繼承Animation類實現動畫,主要是重寫Animation類的applyTransformation(float interpolatedTime, Transformation t)方法來實現自定義動畫效果。要點:動畫在執行過程中會反復的調用applyTransformation()方法,每次調用時參數interpolatedTime都在變化,從0逐漸遞增為1,當該參數為1時表明動畫結束。利用interpolatedTime參數,我們可以逐步改變TextView的高度、TextView的透明度和整個LinearLayout的高度,來達到展開和收起的效果。
// 實現了展開/收起功能的動畫
class ExpandCollapseAnimation extends Animation {
    private final View mTargetView;
    private final int mStartHeight;
    private final int mEndHeight;

    public ExpandCollapseAnimation(View view, int startHeight, int endHeight) {
        mTargetView = view;
        mStartHeight = startHeight;
        mEndHeight = endHeight;
        setDuration(mAnimationDuration);
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        // 計算在執行動畫時,逐漸變化產生的整個View的新高度
        final int newHeight = (int)((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
        // 修改TextView的最大高度
        mTv.setMaxHeight(newHeight - mMarginBetweenTxtAndBottom);
        // 如果初始設置了透明度,修改TextView的透明度,直到alpha==1完全不透明
        if (Float.compare(mAnimAlphaStart, 1.0f) != 0) {
            applyAlphaAnimation(mTv, mAnimAlphaStart + interpolatedTime * (1.0f - mAnimAlphaStart));
        }
        // 修改整個View的高度
        mTargetView.getLayoutParams().height = newHeight;
        mTargetView.requestLayout();
    }
}

// 改變View的透明度
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void applyAlphaAnimation(View view, float alpha) {
    if (isPostHoneycomb()) {
        // setAlpha()方法是Android3.0以上才有的api
        view.setAlpha(alpha);
    } else {
        // 在Android3.0以下,使用AlphaAnimation並將Duration設置為0來實現
        AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha);
        alphaAnimation.setDuration(0);
        alphaAnimation.setFillAfter(true);
        view.startAnimation(alphaAnimation);
    }
}

private static boolean isPostHoneycomb() {
    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}

(7).核心之onClick()方法。
觸發點擊時,Animation開始執行,主要是ExpandCollapseAnimation在構造時startHeight和endHeight的計算。執行“收起”動畫時,從當前高度開始,降低到收起狀態下的高度mCollapsedHeight;執行“展開”動畫時,在當前高度的基礎上,增加TextView變化的高度mTextHeightWithMaxLines - mTv.getHeight()。這裡有個小細節,動畫開始時標記mAnimating為true,結束時標記為false,標記mAnimating變量是為了在onInterceptTouchEvent()方法中使用。這樣,在動畫執行期間,可以攔截掉觸摸事件,不傳遞給子View。
@Override
public void onClick(View view) {
    // 文字內容太短,無需折疊時,箭頭按鈕是隱藏的。這時直接返回。
    if (mButton.getVisibility() != View.VISIBLE) {
        return;
    }

    // 將變量mCollapsed和mButton顯示的圖片賦值為相反
    mCollapsed = !mCollapsed;
    mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable);

    // 將當前位置的狀態保存起來,在ListView中需要使用
    if (mCollapsedStatus != null) {
        mCollapsedStatus.put(mPosition, mCollapsed);
    }

    // 標記動畫開始
    mAnimating = true;

    Animation animation;
    if (mCollapsed) {
        // “收起”動畫,從當前高度開始,降低到收起狀態下的高度mCollapsedHeight
        animation = new ExpandCollapseAnimation(this, getHeight(), mCollapsedHeight);
    } else {
        // “展開”動畫,在當前整個View高度的基礎上,增加TextView變化的高度(mTextHeightWithMaxLines - mTv.getHeight())
        animation = new ExpandCollapseAnimation(this, getHeight(), getHeight() +
                mTextHeightWithMaxLines - mTv.getHeight());
    }

    animation.setFillAfter(true);
    animation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
            // 動畫開始,改變TextView的透明度
            applyAlphaAnimation(mTv, mAnimAlphaStart);
        }
        @Override
        public void onAnimationEnd(Animation animation) {
            // 清除動畫,防止applyTransformation()重復執行
            clearAnimation();
            // 標記動畫結束
            mAnimating = false;

            // 通知listener
            if (mListener != null) {
                mListener.onExpandStateChanged(mTv, !mCollapsed);
            }
        }
        @Override
        public void onAnimationRepeat(Animation animation) { }
    });

    // 清除之前的動畫,開啟新的動畫
    clearAnimation();
    startAnimation(animation);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 動畫執行期間,攔截掉觸摸事件,不傳遞給子View
    return mAnimating;
}

(8).其它方法之setText()。
一般情況下,只需調用setText(text)方法即可。
在ListView等存在item復用的情況下,需要調用setText(text,collapsedStatus,position)方法,這裡使用了SparseBooleanArray記錄當前位置的展開收起狀態。以ListView為例,在實現BaseAdapter類的代碼中,定義成員變量SparseBooleanArray collapsedStatus = new SparseBooleanArray(),當在getView()中調用setText(text,collapsedStatus,position)時,傳入成員變量collapsedStatus和當前的position。

 

// 設置顯示內容
public void setText(@Nullable CharSequence text) {
    mRelayout = true;
    mTv.setText(text);
    // 內容不為空時,才會顯示
    setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
}

// 設置顯示內容,在ListView等存在item復用的情況下需要使用該方法
public void setText(@Nullable CharSequence text, @NonNull SparseBooleanArray collapsedStatus, int position) {
    mCollapsedStatus = collapsedStatus;
    mPosition = position;
    boolean isCollapsed = collapsedStatus.get(position, true);
    clearAnimation();
    mCollapsed = isCollapsed;
    mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable);
    setText(text);
    getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
    requestLayout();
}

(9).其它方法之setOnExpandStateChangeListener()。
設置狀態(“展開”、“收起”)改變的監聽,可以根據實際需求決定是否使用該方法。
public void setOnExpandStateChangeListener(@Nullable OnExpandStateChangeListener listener) {
    mListener = listener;
}

// 狀態改變的監聽器
public interface OnExpandStateChangeListener {
    /**
     * 當展開或收起的動畫執行完畢之後,該方法被回調
     * @param textView - 展開或收起的TextView
     * @param isExpanded - 如果文字被展開,該值為true;否則為false
     */
    void onExpandStateChanged(TextView textView, boolean isExpanded);
}

(10).其它方法之setOrientation()。
在setOrientation()方法中,限定了當前布局的方向只能為垂直。

 

@Override
public void setOrientation(int orientation){
    if(LinearLayout.HORIZONTAL == orientation){
        throw new IllegalArgumentException("ExpandableTextView only supports Vertical Orientation.");
    }
    super.setOrientation(orientation);
}

 

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