功能實現(xiàn)
點擊伸展控件的需求還是很常見, 一般是TextView的伸縮, 因為可能要顯示的文本太多, 一次性展開影響用戶體能, 所以把選擇權(quán)交給用戶, 當然也會有性能優(yōu)化方面的考慮, 這里我給出三種不同方式實現(xiàn)上述需求.
// 1.用Gong & Visible達到效果. 優(yōu)點:簡單; 缺點:浪費資源.
public void setViewIsVisibility(View view){
if(view == null) return;
if (view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
} else{
view.setVisibility(View.GONE);
}
}
第一種實現(xiàn)方式很簡單, 在xml把控件設置為不可以, 然后在監(jiān)聽方法里面反轉(zhuǎn)控件狀態(tài)即可。
// 2.用ViewStub延遲加載布局, [與第一種方式結(jié)合使用.]
if(viewStub.getVisibility() != View.VISIBLE){
viewStub.setVisibility(View.VISIBLE);
llt = ((LinearLayout) findViewById(R.id.llt));
}else {
setViewIsVisibility(llt);
}
第二種實現(xiàn)效果與第一種相同, 不過較第一種更加節(jié)省性能, 這里用到了一個叫ViewStub的控件, 這個控件的寬高都為0,默認為不可見, 當變?yōu)榭梢姇r會把 android:layout="@layout/layout_zomm_content"
這個屬性里的View加載出來.當加載出來后getVisibility()
方法的返回值為0. INVISIBLE = 0x00000004; VISIBLE = 0x00000000; GONE = 0x00000008;
所以我們采用結(jié)合的方式實現(xiàn)伸展效果.
// 3.增加動畫效果, 思路如下: 獲取控件高度, 根據(jù)控件高度做值動畫改變布局高度.
if(mHeight < 0){
mHeight= getViewHeight(); // 因為我們在xml寫的高度為0, 所以要重新測量.
}
----------------------------------------------------------------------
private void executeAnimation() {
if(mHeight < 0) return;
if(llt.getHeight() != 0){
p = mHeight;
s = 0;
}else {
s = mHeight;
p = 0;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) llt.getLayoutParams();
ValueAnimator animator = ValueAnimator.ofInt(p,s);
// 此方法會隨用戶的點擊而調(diào)用, 所以不要用內(nèi)部類的形式創(chuàng)建, 我這里只是節(jié)省代碼量, 增加閱讀性(捂臉)
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (int) animation.getAnimatedValue();
lp.height = animatedValue;
llt.setLayoutParams(lp);
}
});
animator.setDuration(500);
animator.start();
}
第三種方式本來是想在xml中把布局設置為GONE然后獲得布局的高度, 根據(jù)高度來做值動畫, 但是把布局設置為GONE后控件高度為0.
因為無法實現(xiàn)所以只好重新測量控件高度, 測量代碼如下.
public int getViewHeight(){
llt.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
int width = llt.getLayoutParams().width;
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.AT_MOST);
int widthMakeMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
llt.measure(widthMakeMeasureSpec, heightMeasureSpec);
mHeight = llt.getMeasuredHeight();
}
其實這樣寫measure(0, 0) , 也能得到控件的高度, 但我想做為一名有追求的程序員還是搞清楚原因比較好.
View的測量
因為在布局文件中將控件的高度設置成0dp,所以我們首先要做的就是更改0dp, 原因也與view的測量有關(guān), 因為view寬高受各方面影響當把View設置成match_parent
時View的寬高主要受父控件影響, 設置為wrap_content
它又受子View的影響, 只有當把View的寬寫死時它才能自己當家做主, 基于VIew寬高的復雜性設計者把View的最終進行了雙重判斷, 代碼如下 :
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
這個方法是由最外層的FrameLayout調(diào)用, 最外層的View寬高無疑是精確的,包裹整個屏幕的.如果屏幕是480*320
那么windowSize
就是這兩個值中的一個. rootDimension
又是什么呢?其實就是我們在xml中寫的布局屬性layout_widht
值.
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
經(jīng)歷過switch語句后返回一個int類型數(shù)值,我們可以看到返回值就是MeasureSpec的三個常量與layout_xxx
屬性構(gòu)成的.
- MeasureSpec.EXACTLY
表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統(tǒng)默認會按照這個規(guī)則來設置子視圖的大小,開發(fā)人員當然也可以按照自己的意愿設置成任意的大小。
-
MeasureSpec.AT_MOST
表示子視圖最多只能是specSize中指定的大小,開發(fā)人員應該盡可能小得去設置這個視圖,并且保證不會超過specSize。系統(tǒng)默認會按照這個規(guī)則來設置子視圖的大小,開發(fā)人員當然也可以按照自己的意愿設置成任意的大小。 -
MeasureSpec.UNSPECIFIED
表示開發(fā)人員可以將視圖按照自己的意愿設置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
上面的一系列操作其實與我們寫的測量寬度操作是一致的,我們先獲取寬度的屬性參數(shù), 然后把寬度與MeasureSpec.EXACTLY
進行了合成,最后將數(shù)值傳遞給measure
方法.
llt.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
int width = llt.getLayoutParams().width;
int widthMakeMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
llt.measure(widthMakeMeasureSpec, heightMeasureSpec);
measure()
方法會調(diào)用onMeasure()
對于該方法我們一定不佰生, 它是個抽象方法,所以我我繼承View時一定要重寫該方法. 而onMeasure()
方法最終會調(diào)用getDefaultSize()
該方法把我們的之前的數(shù)值進行了解析.
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
AT_MOST與EXACTLY
最終返回值就是measureSpec
中的數(shù)值,如果measureSpec
是由MeasureSpec.EXACTLY
和具體的值構(gòu)成(假如是480)那么最終返回值就是480。就像我們測量的寬度一樣,因為getRootMeasureSpec(desiredWindowWidth, lp.width);
傳入的480
而我們后面也一直用的是match_parnet
所以最后顯示出來的也是480
即包裹整個屏幕。
等等,可你測量高度怎么傳入1000最后成了包裹內(nèi)容呢, 而且為什么傳入0也是同樣的效果?? 那我們就要看是在哪里調(diào)用了getDefaultSize()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(),
heightMeasureSpec));
}
--------------------------------------------------------------
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
還是什么都沒有啊!!! 原因是我們這個時候要看的不是View的onMeasure
方法而具體子類的onMeasure
方法, 我們在自定View時也是在onMeasure
方法對自己的view進行測量的,那傳入的0是什么意思?我們看0對應的是什么模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
可以看到傳入0對應的是UNSPECIFIED
模式, 那子類在獲取模式時就會走UNSPECIFIED的判斷語句里.
ViewGroup如何測量子類的呢?它會調(diào)用下面的方法.
// widthMeasureSpec 與 heightMeasureSpec 就是父View自己的組合值.
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 這是解釋了為什么把View設置為GONEView不占位,也無法獲得高度.
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
// 兒子看爸爸臉色, 兒子的寬度受父親影響.
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
-------------------------------------------------------------------------
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}