点击缩展控件的三种方式

作者: Il_mondo | 来源:发表于2017-02-15 22:49 被阅读172次
    test1

    功能实现

    点击伸展控件的需求还是很常见, 一般是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; 所以我们采用结合的方式实现伸展效果.

    test2.g
    // 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);  
    }  
    

    相关文章

      网友评论

        本文标题:点击缩展控件的三种方式

        本文链接:https://www.haomeiwen.com/subject/xpbqwttx.html