zoukankan      html  css  js  c++  java
  • 【移动开发】怎样自己定义ViewGroup

    本文翻译自《50 android hacks》


    按照惯例。先从一个样例说起。


    非常easy,3张扑克牌叠在一起显示。

    这个布局效果该怎样实现呢?有的同学该说了,这非常easy啊,用RelativeLayout或FrameLayout,然后为每一个扑克牌设置margin就能实现了。

    ok,那就看一下通过这样的方式是怎样实现的。

    代码例如以下:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    
        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:background="#FF0000" />
    
        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:layout_marginLeft="30dp"
            android:layout_marginTop="20dp"
            android:background="#00FF00" />
    
        <View
            android:layout_width="100dp"
            android:layout_height="150dp"
            android:layout_marginLeft="60dp"
            android:layout_marginTop="40dp"
            android:background="#0000FF" />
    
    </RelativeLayout>
    效果图

    没错,通过这样的方式是能够实现的。

    可是。不认为这样的方式有点low吗?!

    让我们用高级一点的方式去实现它,提升一下自己的逼格!

    定制ViewGroup之前,我们须要先理解几个概念。


    Android绘制视图的方式
    这里我不会涉及太多的细节,可是须要理解Android开发文档中的一段话:

    “绘制布局由两个遍历过程组成:測量过程和布局过程。

    測量过程由measure(int, int)方法完毕,该方法从上到下遍历视图树。

    在递归遍历过程中。每一个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每一个视图都保存了各自的尺寸信息。第二个过程由layout(int,int,int,int)方法完毕,该方法也是由上而下遍历视图树。在遍历过程中,每一个父视图通过測量过程的结果定位全部子视图的位置信息。

    简而言之,第一步是測量ViewGroup的宽度和高度,在onMeasure()方法中完毕,ViewGroup遍历全部子视图计算出它的大小。第二步是依据第一步获取的尺寸去布局全部子视图,在onLayout()中完毕。


    创建CascadeLayout

    最终到了定制ViewGroup的阶段了。

    假设我们已经定制了一个CascadeLayout的容器。我们会这样使用它。

    <FrameLayout xmlns:cascade="http://schemas.android.com/apk/res/com.manoel.custom"
        <!-- 声明命名空间 -->
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    
        <com.manoel.view.CascadeLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            <!-- 自己定义属性 -->
            cascade:horizontal_spacing="30dp"
            cascade:vertical_spacing="20dp" >
    
            <View
                android:layout_width="100dp"
                android:layout_height="150dp"
                android:background="#FF0000" />
    
            <View
                android:layout_width="100dp"
                android:layout_height="150dp"
                android:background="#00FF00" />
    
            <View
                android:layout_width="100dp"
                android:layout_height="150dp"
                android:background="#0000FF" />
        </com.manoel.view.CascadeLayout>
    
    </FrameLayout>

    首先,定义属性。在values目录以下创建attrs.xml。代码例如以下:

    <resources>
        <declare-styleable name="CascadeLayout">
            <attr name="horizontal_spacing" format="dimension" />
            <attr name="vertical_spacing" format="dimension" />
        </declare-styleable>
    </resources>
    
    同一时候,为了严谨一些,定义一些默认的垂直距离和水平距离,以防在布局中没有提供这些属性。

    在dimens.xml中加入例如以下代码:

    <resources>
        <dimen name="cascade_horizontal_spacing">10dp</dimen>
        <dimen name="cascade_vertical_spacing">10dp</dimen>
    </resources>
    准备工作已经做好了。接下来看一下CascadeLayout的源代码,稍微有点长,后面帮助大家分析一下。

    public class CascadeLayout extends ViewGroup {
    
      private int mHorizontalSpacing;
      private int mVerticalSpacing;
    
      public CascadeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    
        TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.CascadeLayout);
    
        try {
          mHorizontalSpacing = a.getDimensionPixelSize(
              R.styleable.CascadeLayout_horizontal_spacing,
              getResources().getDimensionPixelSize(
                  R.dimen.cascade_horizontal_spacing));
    
          mVerticalSpacing = a.getDimensionPixelSize(
              R.styleable.CascadeLayout_vertical_spacing, getResources()
                  .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
        } finally {
          a.recycle();
        }
    
      }
    
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getPaddingLeft();
        int height = getPaddingTop();
        int verticalSpacing;
    
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
          verticalSpacing = mVerticalSpacing;
    
          View child = getChildAt(i);
          measureChild(child, widthMeasureSpec, heightMeasureSpec);
    
          LayoutParams lp = (LayoutParams) child.getLayoutParams();
          width = getPaddingLeft() + mHorizontalSpacing * i;
    
          lp.x = width;
          lp.y = height;
    
          if (lp.verticalSpacing >= 0) {
            verticalSpacing = lp.verticalSpacing;
          }
    
          width += child.getMeasuredWidth();
          height += verticalSpacing;
        }
    
        width += getPaddingRight();
        height += getChildAt(getChildCount() - 1).getMeasuredHeight()
            + getPaddingBottom();
    
        setMeasuredDimension(resolveSize(width, widthMeasureSpec),
            resolveSize(height, heightMeasureSpec));
      }
    
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
          View child = getChildAt(i);
          LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
              + child.getMeasuredHeight());
        }
      }
    
      @Override
      protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
      }
    
      @Override
      protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT,
            LayoutParams.WRAP_CONTENT);
      }
    
      @Override
      public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
      }
    
      @Override
      protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p.width, p.height);
      }
    
      public static class LayoutParams extends ViewGroup.LayoutParams {
        int x;
        int y;
        public int verticalSpacing;
    
        public LayoutParams(Context context, AttributeSet attrs) {
          super(context, attrs);
        }
    
        public LayoutParams(int w, int h) {
          super(w, h);
        }
    
      }
    }
    

    首先,分析构造函数。

    public CascadeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    
        TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.CascadeLayout);
    
        try {
          mHorizontalSpacing = a.getDimensionPixelSize(
              R.styleable.CascadeLayout_horizontal_spacing,
              getResources().getDimensionPixelSize(
                  R.dimen.cascade_horizontal_spacing));
    
          mVerticalSpacing = a.getDimensionPixelSize(
              R.styleable.CascadeLayout_vertical_spacing, getResources()
                  .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
        } finally {
          a.recycle();
        }
    
      }
    假设在布局中使用CasecadeLayout,系统就会调用这个构造函数,这个大家都应该知道的吧。这里不解释why。有兴趣的能够去看源代码,重点看系统是怎样解析xml布局的。

    构造函数非常easy,就是通过布局文件里的属性,获取水平距离和垂直距离。


    然后。分析自己定义LayoutParams。

    这个类的用途就是保存每一个子视图的x,y轴位置。这里把它定义为静态内部类。ps:提到静态内部类。我又想起来关于多线程内存泄露的问题了,假设有时间再给大家解释一下多线程造成内存泄露的问题。

    public static class LayoutParams extends ViewGroup.LayoutParams {
        int x;
        int y;
        public int verticalSpacing;
    
        public LayoutParams(Context context, AttributeSet attrs) {
          super(context, attrs);
        }
    
        public LayoutParams(int w, int h) {
          super(w, h);
        }
    
      }

    除此之外。还须要重写一些方法。checkLayoutParams()、generateDefaultLayoutParams()等,这种方法在不同ViewGroup之间往往是同样的。


    接下来。分析onMeasure()方法。

      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getPaddingLeft();
        int height = getPaddingTop();
        int verticalSpacing;
    
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
          verticalSpacing = mVerticalSpacing;
    
          View child = getChildAt(i);
          measureChild(child, widthMeasureSpec, heightMeasureSpec); // 令每一个子视图測量自身
    
          LayoutParams lp = (LayoutParams) child.getLayoutParams();
          width = getPaddingLeft() + mHorizontalSpacing * i;
          // 保存每一个子视图的x。y轴坐标
          lp.x = width;
          lp.y = height;
    
          if (lp.verticalSpacing >= 0) {
            verticalSpacing = lp.verticalSpacing;
          }
    
          width += child.getMeasuredWidth();
          height += verticalSpacing;
        }
    
        width += getPaddingRight();
        height += getChildAt(getChildCount() - 1).getMeasuredHeight()
            + getPaddingBottom();
        // 使用计算所得的宽和高设置整个布局的測量尺寸
        setMeasuredDimension(resolveSize(width, widthMeasureSpec),
            resolveSize(height, heightMeasureSpec));
      }


    最后,分析onLayout()方法。

      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
          View child = getChildAt(i);
          LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
              + child.getMeasuredHeight());
        }
      }
    逻辑非常easy。用onMeasure()方法计算出的值为參数循环调用子View的layout()方法。


    为子视图加入自己定义属性

    作为演示样例。以下将加入子视图重写垂直间距的方法。

    第一步是向attrs.xml中加入一个新的属性。

        <declare-styleable name="CascadeLayout_LayoutParams">
            <attr name="layout_vertical_spacing" format="dimension" />
        </declare-styleable>

    这里的属性名是layout_vertical_spacing,由于该属性名前缀是layout_。同一时候,又不是View固有的属性。所以该属性会被加入到LayoutParams的属性表中。在CascadeLayout类的构造函数中读取这个新属性。

    public static class LayoutParams extends ViewGroup.LayoutParams {
        int x;
        int y;
        public int verticalSpacing;
    
        public LayoutParams(Context context, AttributeSet attrs) {
          super(context, attrs);
    
          TypedArray a = context.obtainStyledAttributes(attrs,
              R.styleable.CascadeLayout_LayoutParams);
          try {
            verticalSpacing = a
                .getDimensionPixelSize(
                    R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,
                    -1);
          } finally {
            a.recycle();
          }
        }
    
        public LayoutParams(int w, int h) {
          super(w, h);
        }
    
      }
    

    那怎么使用这个属性呢?so easy!

        <com.manoel.view.CascadeLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            cascade:horizontal_spacing="30dp"
            cascade:vertical_spacing="20dp" >
    
            <!-- 注意:这张“扑克牌”使用了layout_vertical_spacing属性 -->
            <View
                android:layout_width="100dp"
                android:layout_height="150dp"
                cascade:layout_vertical_spacing="90dp"
                android:background="#FF0000" />
    
            <View
                android:layout_width="100dp"
                android:layout_height="150dp"
                android:background="#00FF00" />
    
            <View
                android:layout_width="100dp"
                android:layout_height="150dp"
                android:background="#0000FF" />
        </com.manoel.view.CascadeLayout>


    參考资料

  • 相关阅读:
    react ts axios 配置跨域
    npm run eject“Remove untracked files, stash or commit any changes, and try again.”错误
    java 进程的参数和list的线程安全
    帆软报表 大屏列表跑马灯效果JS
    帆软报表 快速复用数据集,避免重复劳动
    分析云 OA中部门分级思路和实现方法
    分析云 分段器 只显示一个块的数据
    分析云 更改服务默认的端口号
    分析云U8项目配置方法新版本(2)
    Oracle 创建时间维度表并更新是否工作日字段
  • 原文地址:https://www.cnblogs.com/bhlsheji/p/5353842.html
Copyright © 2011-2022 走看看