Android:从绘制一张流程图来体验View的自定义过程(一)

 

概述

        自定义View向来不是一个轻松的话题。虽然对于android原生开发而言,这只能算是一项基本的技能,但真正当我们拿到一份需要我们自定义去实现的需求的时候,多少还会有点手足无措。具体是什么原因,我也不知道,也许,“菜,是原罪”。‘’“业精于勤,荒于嬉”,也许就是我们每一次需要的去践行这种需求的时候,内心的那一点点惰性促使我们刻意地绕过这种实践机会。久而久之,对于同一种事物的排斥感与畏惧感就与日俱增。所以,砖还是要搬的,代码还是要敲的。自定义View,还是需要去自定义的。

       学习自定义View,第一步得先明白为什么要自定义View。这个问题感觉有点弱智。但仔细想一想,这个问题确实是有值得思考的空间。我们不能因为敲代码而去敲代码,更不能因为自定义View而去自定义View。前端View这个组件出现的目的是什么?简而言之,交互。换句话说,我们需要通过这个View展示我们需要展示给用户的信息,同时也需要这个View获取用户希望传递给我们的信息。如果以目前的我们所知道的组件无法满足我们交互的时候,我们就应该考虑自定义View了。感觉说了这些跟没说了一样,其实我想说的是,实际上很多的需求我们都是可以通过一些其他的技术,比如说动画,组合控件等来实现我们交互上的需求,没有必要去自定义View。自定义View的时间成本比较大。需要考虑的问题比较多,维护与测试的时间长,在这个注重敏捷开发的时代,还是要比较慎重的。

        如果已经确定了需要走自定义View这条路,则接下来需要选择去确定自定义View的方式。自定义View的方式有三种:1.将系统中原有组件组合,实现新的View 或者layout。这种方式比较简单。2.对系统原有的组件进行改造。这也是比较常见的,比如说ImageView实现圆角。3.则是完完全全的去定义实现一个具备新的交互模式的View。

        今天的需求就是从无到有的去实现一个全新的View。这个View的需求是一个订单的流程图。具体效果如下

 

        需求分析。初次看到这张图。感觉实现的方式有很多种。用组合控件的方式可以很方便的实现这种需求。一个LineaLayout中横向布局四个Tab,上面是ImageView,下面是TextView,中间用背景颜色是黄色的View连接。感觉没有必要自定义View。但是。我需要说明的是,通过组合控件的方式确实是可以实现这种需求,但是在涉及到比较多的界面的时候,是不是每一处,我们都要手动的去修改View的状态(修改Tab的背景图,文件颜色,连接线的颜色)?如果我们把这些UI的变化作为View本身内部的一种绘画机制,从某种意义上说,是不是也是一种代码上的封装。反正我觉得是。

      在我决定要自定义的去实现这个View的时候,我已经开始根据UI给的需求考虑这个View的扩展性了。自定义的这个View我把它取名叫做StepView,步骤图。任何一个自定义View的扩展性或者说耦合性都应该是我们重点考虑的点,这个我的个人观点,换句话说,自定义View的产生可能是基于某种特别的需求,但是,它的出现不应该仅仅局限于这种需求。所以,扩展性的考虑应该是非常重要的方面。对于UI给的这个图,对于功能扩展性的考虑如下(常规,很多情况下要结合具体的实际):

  1. 步骤图中每个步骤的图标是可以定制的(样式,大小,默认图标(选中,非选中));
  2. 步骤图中的每个图标之间的连接线应该是可以定制的(颜色(选中与没选中),宽度);
  3. 步骤图中的每个步骤的文字说明应该是可以定制的(内容,大小,颜色);
  4. 步骤图的总步骤数应该是可以是可以动态设定的,当前所在步骤数也是可以动态设定的;

而这些扩展性的实现需要用到自定义View当中的自定义属性;

自定义属性

        定义属性

        在模块目录res--values目录下新建attrs.xml文件。在目录中定义自己需要的自定义属性,如下图:(属性名字应该可以不用注释吧)

   <declare-styleable name="StepView">
        <attr name="step_iconSize" format="dimension" />
        <attr name="step_lineColor" format="color|reference" />
        <attr name="step_lineSelectedColor" format="color|reference" />
        <attr name="step_textColor" format="color|reference" />
        <attr name="step_textSelectedColor" format="color|reference" />
        <attr name="step_textSize" format="dimension" />
        <attr name="step_stepCount" format="integer" />
        <attr name="step_currentStep" format="integer" />
        <attr name="step_padding" format="dimension" />
        <attr name="step_icon" format="reference" />
        <attr name="step_selected_icon" format="reference" />
    </declare-styleable>

        自定义属性的formate属性可以为

  •         
    • refrence:引用(@string ,@color)
    • integer:  整数(1,2,3,4)
    • dimention 尺寸(dp,dip ,sp,dx)
    • color 颜色(#10101011)
    • string 字符串("abcedefg")
    • boolean 布尔值(true,false)
    • enum 枚举(需要去自定义,比较少用到)
    • flag  标志(比较少用到)
    • float 小数类型(1.0f ,2.22f)
    • fraction 百分比(24%,12%)

        获取自定义属性

        View的构造函数有高几个,基本上我们只需要实现两个。一个是在java代码中可以实例化的构造函数,另一个则是在XML文件中实例化的构造函数,具体如下

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

    public StepView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

 在获取自定义属性的时候需要注意的点

1.TypeArray记得回收,不然会造成内存泄漏。

2.自定义属性记得设置默认值。避免在后面的业务逻辑中造成空指针异常。

3.需要时刻注意属性的单位问题(在View的绘制代码中基本上用的单位是px,但是我们在xml文件中配置的都是dp单位,所以,这里面就涉及到一个单位的转化问题,而对于获取一些dimension的值的时候,发现有好几个api,求助了百度后,他们基本上都说是单位的转换问题,自己动手敲了一下,发现实际上并不是单位的的转换问题(dp和px,sp转换))。

  • getDimension:

  • getDimensionPixelSize:

  • getDimensionPixelOffset:

一开始遇到这个问题,求助了百度,大家说的好像都是涉及到dp与sp如何转化为px,有些api不会乘以手机的屏幕像素密度的问题,其实会有这些误解,基本上的原因是因为国内的手机定制厂商没有为这些原生Google api做适配的问题。这些API的的主要功能基本上都是相同的,都是获取我们定义的Dimension的值,并且都会转化为px单位,无论我们在xml文件中定义的sp,dp,px,他们都会非常智能的帮我们转换为px单位。他们最主要的不同点在于我们在xml文件中定义dimension时候是否使用了小数。换句话说,如果你确定你的xml文件中的dimension只定义了integer数据,那上面三个api没有任何差别;先看看上面三者的方法说明就ok了(Google的代码注释真的非常有用)

getDimension:根据特定的资源ID获取Dimension,单位的转换涉及到资源文件中的当前identy的值(屏幕像素密度);

 /**
     * Retrieve a dimensional for a particular resource ID.  Unit 
     * conversions are based on the current {@link DisplayMetrics} associated
     * with the resources.
     * /

getDimensionPixelSize:他的基本功能跟getDimension是一样的。唯一的不同点就是返回的维度大小是以int作为单位,并且返回值的转换是基于四舍五入的原则,所以必须要保证它有一个非零的基值。

  /**
     * Retrieve a dimensional for a particular resource ID for use
     * as a size in raw pixels.  This is the same as
     * {@link #getDimension}, except the returned value is converted to
     * integer pixels for use as a size.  A size conversion involves
     * rounding the base value, and ensuring that a non-zero base value
     * is at least one pixel in size.*/

getDimensionPixelOffset:基本功能跟getDimension一样,不同点在于返回的值是int,并且转换原则是取整

  /**
     * Retrieve a dimensional for a particular resource ID for use
     * as an offset in raw pixels.  This is the same as
     * {@link #getDimension}, except the returned value is converted to
     * integer pixels for you.  An offset conversion involves simply
     * truncating the base value to an integer.
     * /

上面的三个方法注释说明看明白了,则基本上可以确定自己在代码中需要用到哪个api了。接下来则需要在代码中获取这些属性值了。

   public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
//        // get the styleArray;
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StepView);
        if (typedArray != null) {
            // icon 大小
            mIconSize = typedArray.getDimensionPixelSize(R.styleable.StepView_step_iconSize,
                    default_icon_size);
            LogUtils.d(TAG, "iconSize:" + mIconSize);
            // 文字颜色;
            mTextColor = typedArray.getColor(R.styleable.StepView_step_textColor, mContext
                    .getResources().getColor(R.color.step_default_color));
            mTextSelectedColor = typedArray.getColor(R.styleable
                    .StepView_step_textSelectedColor, mContext
                    .getResources().getColor(R.color.step_selected_color));
            LogUtils.d(TAG, "textColor:" + mTextColor + "  textSelectedColor:" +
                    mTextSelectedColor);
            // 线的颜色;
            mLineColor = typedArray.getColor(R.styleable.StepView_step_lineColor, mContext
                    .getResources().getColor(R.color.step_default_color));
            mLineSelectedColor = typedArray.getColor(R.styleable
                    .StepView_step_lineSelectedColor, mContext
                    .getResources().getColor(R.color.step_selected_color));
            LogUtils.d(TAG, "lineColor:" + mLineColor + "  lineSelectedColor:" +
                    mLineSelectedColor);
            // 文字大小
            mTextSize = typedArray.getDimensionPixelSize(R.styleable
                    .StepView_step_textSize, default_text_size);
            LogUtils.d(TAG, "texSize:" + mTextSize);
            // icon与文字之间的间距
            mPadding = typedArray.getDimensionPixelSize(R.styleable
                    .StepView_step_padding, 10 * 3);
            LogUtils.d(TAG, "padding:" + mPadding);
            // 总的步骤数;
            mStepCount = typedArray.getInt(R.styleable.StepView_step_stepCount,
                    default_icon_size);
            LogUtils.d(TAG, "stepCount:" + mStepCount);
            // 当前步骤;
            mCurrentStep = typedArray.getInt(R.styleable.StepView_step_currentStep, 0);
            LogUtils.d(TAG, "currentStep:" + mCurrentStep);

            mDefaultIcon = typedArray.getDrawable(R.styleable.StepView_step_icon);
            if (mDefaultIcon == null) {
                mDefaultIcon = mContext.getResources().getDrawable(R.mipmap.step_icon_default);
            }
            LogUtils.d(TAG, "icon:" + mDefaultIcon);
            mSelectedIcon = typedArray.getDrawable(R.styleable
                    .StepView_step_selected_icon);
            if (mSelectedIcon == null) {
                mSelectedIcon = mContext.getResources().getDrawable(R.mipmap.step_icon_selected);
            }
            LogUtils.d(TAG, "selectedIcon:" + mSelectedIcon);
        }
        // recycly the typeArray;
        typedArray.recycle();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

typeArray属于需要在使用完后积极的回收,不然会造成内存泄漏。这些工作做完后,我们就可以直接在我们的xml文件中引用我们自定义的属性了。

使用自定义属性

    <com.aikeding.commentlibrary.widget.stepview.StepView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:step_iconSize="25dp"
        app:step_stepCount="5" />

           在这里,记录一个问题,很多时候我们使用android studio IDE的时候,自定义的View的属性没办法自定提示,这个时候我们需要检查下面三个步骤:

  1. 命名空间问题:检查命名空间是否已经填写了。android studio现在已经很智能了,命名空间可以自动生成;
  2. 自定义属性文件错误问题:检查自定义属性xml文件是否存在错误,xml中的自定义属性名最好跟自己的自定义View相同;
  3. 构建版本的问题:很多情况下我们的自定义View会放在自己的包工程中,检查包模块与使用模块的gradle构建版本相同,如果不相同,很大情况下会导致自定义属性无法提示的问题

基本上面三个问题排除后,问题可以解决;

猜你喜欢

转载自blog.csdn.net/yuguqinglei/article/details/81120457