功能实现
点击伸展控件的需求还是很常见, 一般是TextView的伸缩, 因为可能要显示的文本太多, 一次性展开影响用户体能, 所以把选择权交给用户, 当然也会有性能优化方面的考虑, 这里我给出三种不同方式实现上述需求.
// 1.用Gong & Visible达到效果. 优点:简单; 缺点:浪费资源.
public void setViewIsVisibility(View view){
if(view == null) return;
if (view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
} else{
view.setVisibility(View.GONE);
}
}
第一种实现方式很简单, 在xml把控件设置为不可以, 然后在监听方法里面反转控件状态即可。
// 2.用ViewStub延迟加载布局, [与第一种方式结合使用.]
if(viewStub.getVisibility() != View.VISIBLE){
viewStub.setVisibility(View.VISIBLE);
llt = ((LinearLayout) findViewById(R.id.llt));
}else {
setViewIsVisibility(llt);
}
第二种实现效果与第一种相同, 不过较第一种更加节省性能, 这里用到了一个叫ViewStub的控件, 这个控件的宽高都为0,默认为不可见, 当变为可见时会把 android:layout="@layout/layout_zomm_content"
这个属性里的View加载出来.当加载出来后getVisibility()
方法的返回值为0. INVISIBLE = 0x00000004; VISIBLE = 0x00000000; GONE = 0x00000008;
所以我们采用结合的方式实现伸展效果.
// 3.增加动画效果, 思路如下: 获取控件高度, 根据控件高度做值动画改变布局高度.
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);
// 此方法会随用户的点击而调用, 所以不要用内部类的形式创建, 我这里只是节省代码量, 增加阅读性(捂脸)
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然后获得布局的高度, 根据高度来做值动画, 但是把布局设置为GONE后控件高度为0.
因为无法实现所以只好重新测量控件高度, 测量代码如下.
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的测量有关, 因为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调用, 最外层的View宽高无疑是精确的,包裹整个屏幕的.如果屏幕是480*320
那么windowSize
就是这两个值中的一个. rootDimension
又是什么呢?其实就是我们在xml中写的布局属性layout_widht
值.
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
经历过switch语句后返回一个int类型数值,我们可以看到返回值就是MeasureSpec的三个常量与layout_xxx
属性构成的.
- MeasureSpec.EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
-
MeasureSpec.AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 -
MeasureSpec.UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
上面的一系列操作其实与我们写的测量宽度操作是一致的,我们先获取宽度的属性参数, 然后把宽度与MeasureSpec.EXACTLY
进行了合成,最后将数值传递给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()
方法会调用onMeasure()
对于该方法我们一定不佰生, 它是个抽象方法,所以我我继承View时一定要重写该方法. 而onMeasure()
方法最终会调用getDefaultSize()
该方法把我们的之前的数值进行了解析.
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
中的数值,如果measureSpec
是由MeasureSpec.EXACTLY
和具体的值构成(假如是480)那么最终返回值就是480。就像我们测量的宽度一样,因为getRootMeasureSpec(desiredWindowWidth, lp.width);
传入的480
而我们后面也一直用的是match_parnet
所以最后显示出来的也是480
即包裹整个屏幕。
等等,可你测量高度怎么传入1000最后成了包裹内容呢, 而且为什么传入0也是同样的效果?? 那我们就要看是在哪里调用了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如何测量子类的呢?它会调用下面的方法.
// 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);
}