zoukankan      html  css  js  c++  java
  • android自定义控件一站式入门

    TODO: 待整理

    # 自定义控件 Android系统提供了一系列UI相关的类来帮助我们构造app的界面,以及完成交互的处理。 一般的,所有可以在窗口中被展示的UI对象类型,最终都是继承自View的类,这包括展示最终内容的非布局View子类和继承自ViewGroup的布局类。 其它的诸如Scroller、GestureDetector等android.view包下的辅助类简化了有关视图操作和交互处理。 无论如何,自己的app中总会遇到内建的类型无法满足要求的场景,这时就必须实现自己的UI组件了。

    自定义控件的方式

    根据需要,有以下几个方式来完成自定义控件:

    • 继承View或ViewGroup类
      这种情况是你需要完全控制视图的内容展示和交互处理的情况下,直接继承View类可以获得最大限度的定制。

    • 继承特定的View子类
      如果内建的某个View子类基本符合使用要求,只是需要定制该View某些方面的功能时,选择此种方式。
      例如继承TextView为其增加特殊的文字显示效果,竖排显示等。

    • 组合已有View
      组合View实现自定义控件其实主要就是为了完成组合成后的目标View的复用。这里组合就是定义一个ViewGroup的子类,然后添加需要的childView。
      典型的有EditText + ListView实现Combox(下拉框)这样的东西。做法就是继承布局类,然后inflate对应布局文件or代码中创建(蛋疼)作为其包含的child views。
      统一的搜索栏,级联菜单等,组合控件其实有点类似布局中include这样的做法,如果为一个可复用的片段layout配一个ViewManager,效果几乎是一样的。当然,自定义控件的好处就是可以在xml中直接声明,而且UI和对应逻辑是集中管理的。便于复用。

    案例:PieChart控件

    自定义控件的几种方式中,直接继承自View类的方式包含自定义View用到的完整的开发技巧。接下来将以官方文档Develop > Training > Best Practices for User Interface > Creating Custom Views中讲述的PieChart自定义控件为例,了解下自定义View的开发流程。

    功能目标:

    将要实现的PieChart控件如下图:

    PieChart示例图 PieChart的图形组成
    具有以下主要功能目标:
    • PieChart需要展示一个由一或多个扇形组成的圆,一个在圆的固定位置的指示圆点,一个在圆的左侧或右侧固定位置的标签。
    • 圆的每个扇形表示一个显示项(Item)。可以添加任意多个Item,每个Item有它的color、value、label来确定扇形的显示。所有扇形根据其添加顺序顺时针从0°开始组成整个圆。如上面的是包含红、绿、蓝,值分别为1、2、3的三个Item组成的圆。
    • 手指滑动时转动饼状图,滑动方向与圆心到滑动方向的直线决定了转动方向。例如手指处在圆心下方时向左滑动时圆顺时针转动。
    • 圆转动时,指示圆点落在那个扇形的区域,扇形对应的Item就是当前Item。它对应的label内容被显示。
    • 手指快速划过后(fling——具有flywheel效果),饼状图以动画的方式慢慢停止而不是立即停止转动。
    • 滑动(包括fling)结束后,居中当前项——指示点在当前项对应扇形角度中心。

    以上是要实现的自定义控件PieChart需要满足的业务要求。下面就一步步设计和完成PieChart控件。

    基础工作

    在开始实现控件的功能目标之前,需要做一些基础工作,让自己的控件可以运行调试。之后再逐步完成显示和交互功能。

    1. 创建PieChart类:

    1.1 ViewGroup和Viw的选择

    View只能显示内容,而ViewGroup可以包含其他View或ViewGroup。ViewGroup本身也是View的子类,它也可以显示内容。
    为了让PieChart可以同时显示标签和圆,可以使用一个单独的View子类来绘制,但是,这里选择让PieChart作为一个ViewGroup,
    它来显示标签和指示圆点,然后设计一个PieView类来完成圆的绘制。

    这样做有以下好处是:

    1. 在Android 3.0(API 11)之后,引入了硬件加速特性,在执行一些动画时可以提升UI体验。但是启用硬件加速需要更多的内存开销。
      对于需要转动和使用动画效果的圆来说,在它执行动画的时候可以开启硬件加速,动画停止的时候取消硬件加速。分多个View可以在独立的硬件加速层绘制圆,又避免了标签和指示圆点这样写图形不需要加速的事实。
    2. 分开两个View,可以让逻辑更加清晰,避免一个类过度复杂(出于演示目的)。
    3. PieChart继承ViewGroup,PieView继承View,这样可以在当前案例中同时介绍到自定义View相关的“测量、布局和绘制”的知识。

    1.2 构造器和布局xml创建

    控件对象应该可以是通过代码或xml方式创建。
    通过xml方式定义的控件在创建时执行的是包含Context和AttributeSet两个参数的构造器,为了可以在xml中定义控件对象,PieChart类就需要提供此构造器:

    public class PieChart extends ViewGroup {
      public PieChart(Context context) {
          super(context);
          init();
      }
    
      public PieChart(Context context, AttributeSet attrs) {
          super(context, attrs);
          getAttributes(context, attrs);
          init();
      }
      ...
    }
    

    额外的AttributeSet参数携带了在xml中为控件指定的attribute集合。attribute表示可以在布局xml文件中定义View时使用的xml元素名称,例如layput_width,padding这样的。这些attribute相当于在定义控件对象的时候提供的初始值,更直接点,类似于构造函数的参数。

    Android提供了统一的通过xml为创建的控件对象提供初始值的方式:

    1. 为控件定义xml中使用的attribute。
    2. 在布局文件中为控件使用这些attribute。
    3. 构造器通过AttributeSet参数获得xml中定义的这些attribute值。

    接下来的1.2和1.3分别介绍如何定义attribute,以及如何使用attribute。

    attribute和property都翻译为属性,attribute表示可以在布局xml文件中定义View时使用的xml元素名称,例如layput_width,padding这样的。而property表示类的getter/setter或者类似的对某个private字段的访问方法。

    2. 提供和使用自定义属性

    2.1 定义attribute

    首先,在res/values/attrs.xml文件中定义属性:

    <resources>
       <declare-styleable name="PieChart">
           <attr name="showText" format="boolean" />
           <attr name="labelHeight" format="dimension"/>
           <attr name="pointerRadius" format="dimension"/>
           <attr name="labelPosition" format="enum">
               <enum name="left" value="0"/>
               <enum name="right" value="1"/>
           </attr>
       </declare-styleable>
    </resources>
    

    对应每个View类,使用一个declare-styleable为其定义相关的属性。
    类似color、string等资源那样,每一个使用attr标签定义的属性,在R.styleable类中会生成一个对应的静态只读int类型的字段作为其id。
    例如上面的pointerRadius属性在对应R.styleable.PieChart_pointerRadius属性。

    public static final int PieChart_pointerRadius = 8;
    

    2.2 使用attribute

    在attr.xml中定义好属性后,布局文件中,声明控件的地方就可以指定这些属性值了:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
     <com.example.customviews.charting.PieChart
         custom:showText="true"
         custom:pointerRadius="4dp"
         custom:labelPosition="left" />
    </LinearLayout>
    

    因为是引入的额外属性,不是android内置的属性(Android自身在sdk下资源attr.xml中定义好了内置各个View相关的属性),需要使用一个不同的xml 命名空间来引用我们的属性。
    上面xmlns:custom=的声明是一种引入的方式,格式是

    http://schemas.android.com/apk/res/[your package name]
    

    另一种简单的方式是

    xmlns:app="http://schemas.android.com/apk/res-auto"
    

    这样所有自定义属性都可以使用app:attrName这样的方式被使用了。

    xml中定义控件对象的标签必须是类全名称,而且自定义控件类是内部类时,需要这样使用:

    <View
        class="com.android.notepad.NoteEditor$MyEditText"
        custom:showText="true"
        custom:labelPosition="left" />
    

    3. 获取并使用自定义属性

    在控件类PieChart中,在构造器中通过AttributeSet参数获得xml中定义的属性值:

    public class PieChart extends ViewGroup {
        public PieChart(Context context, AttributeSet attrs) {
           super(context, attrs);
           getAttributes(context, attrs);
           init();
        }
    
        private void getAttributes(Context context, AttributeSet attrs) {
          TypedArray a = context.getTheme().obtainStyledAttributes(
               attrs, R.styleable.PieChart, 0, 0);
    
          try {
              mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
              mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
          } finally {
              a.recycle();
          }
        }
    }
    

    再次强调,xml中定义的对象最终被创建时所执行的构造器就是含Context和TypedArray两个参数的构造器。
    上面在构造方法中,必须调用super(context, attrs),因为父类View本身也有许多attribute需要解析。getAttributes方法首先获得一个TypedArray对象,根据R.styleable类中对应每个attribute的id字段从TypedArray对象中获取attribute的值。
    解析到attribute值后,赋值给对应的字段,这样就完成了在xml中为控件对象提供初始值的目标。

    TypedArray是一个共享的资源对象,使用完毕就立即执行recycle释放对它的占用。

    4. 暴露property和事件

    4.1 控件属性(property)

    一方面可以通过xml中使用attribute来为控件对象提供初始值,类似其它java类那样,为了在代码中对控件相关状态进行操作,需要提供这些属性的访问方法。
    控件类是和屏幕显示相关的类,它的很多状态都和其显示的最终内容相关。最佳实践是:总是暴露那些影响控件外观和行为的属性。

    对于PieChart类,字段textHeigh用来控制显示当前项对应标签文本的高度,字段pointerRadius用来控制显示的指示圆点的半径。
    为了能控制其当前项标签的文本高度,或者当前项指示圆点的半径,需要公开对这些字段的访问:

    class PieChart extends ViewGroup {
      ...
    
      // 属性
      public float getTextHeight() {
          return mTextHeight;
      }
    
      public void setTextHeight(float textHeight) {
         mTextHeight = textHeight;
         invalidate();
      }
    
      public float getPointerRadius() {
          return mPointerRadius;
      }
    
      public void setPointerRadius(float pointerRadius) {
          mPointerRadius = pointerRadius;
          invalidate();
      }
    }
    

    textHeigh和pointerRadius这样的属性的改变会导致控件外观发生变化,这时需要同步其UI显示和内容数据,invalidate方法通知系统此View的展示区域已经无效了需要重新绘制。当控件大小发生变化时,requestLayout请求重新布局当前View对象的可见位置。
    在关键属性被修改后,应该重绘view,或者还要重新布局view对象在屏幕的显示区域。保证其状态和显示统一。

    4.2 控件事件

    控件会在交互过程中产生各种事件,自定义控件根据需要也要暴露出专有的用户交互事件被监听处理。
    PieChart类在转动的时候,指示圆点指示的当前项会发生变化。
    所以这里定义接口OnCurrentItemChanged来供使用者来监听当前项的变化:

    class PieChart extends ViewGroup {
      ...
    
      // 事件
      private OnCurrentItemChangedListener mCurrentItemChangedListener = null;
    
      public interface OnCurrentItemChangedListener {
          void OnCurrentItemChanged(PieChart source, int currentItem);
      }
    
      public void setOnCurrentItemChangedListener(OnCurrentItemChangedListener listener) {
          mCurrentItemChangedListener = listener;
      }
    }
    
    

    5. 基础工作小结

    在定义了PieChart对象,为其提供可attribute,在布局中声明了控件对象,提供了构造器中获得这些attribute的方法,以及简单的几个属性和事件定义完成之后,现在可以运行查看控件的运行效果了。
    目前它还没有任何内容显示和交互,但我们完成了基础工作。
    接下来,将会不断加入更多的字段、方法来实现PieChart控件的功能目标。

    实现绘制过程

    为了实现PieChart的最终正确显示涉及到好几步操作,首先我们尝试(如果有遇到其它技术问题,会暂停,然后分析该问题的解决,之后再回到上级问题本身)从绘制其显示内容的方法onDraw开始。

    6. 理解onDraw方法

    控件绘制其内容是在onDraw方法中进行的,方法原型:

    protected void onDraw(Canvas canvas);
    

    Canvas类表示画布:它定义了一系列方法用来绘制文本、线段、位图和一些基本图形。自定义View根据需要使用Canvas来完成自己的UI绘制。
    另一个绘制需要用到的类是Paint。
    android.graphics包下衍生出了两个方向:

    • Canvas处理绘制什么的问题。
    • Paint处理怎么绘制的问题。

    例如,Canvas定义了一个方法用来画线段,而Paint可以定义线段的颜色。Canvas定义了方法画矩形,而Paint可以定义是否以固定颜色填充矩形或保持矩形内部为空。简而言之,Canvas定义了可以在屏幕上绘制的图形,Paint定义了绘制使用的颜色、字体、风格、以及和图形相关的其它属性。

    所以,为了在onDraw()方法传递的Canvas画布上绘制内容之前,需要准备好画笔对象。
    根据需要,可以创建多个画笔来绘制不同的图形。因为绘图相关对象的创建都比较耗费性能,而onDraw方法调用频率很gao(PieChart是可以转动的,每次转动都需要重新执行onDraw)。所以对Paint对象的创建放在PieChart对象创建时——也就是构造器中执行。下面定义了init()方法完成Paint对象的创建以及一些其它的初始化任务:

    public class PieChart extends ViewGroup {
      private void init() {
         mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
         mTextPaint.setColor(mTextColor);
         if (mTextHeight == 0) {
             mTextHeight = mTextPaint.getTextSize();
         } else {
             mTextPaint.setTextSize(mTextHeight);
         }
    
         mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
         mPiePaint.setStyle(Paint.Style.FILL);
         mPiePaint.setTextSize(mTextHeight);
         ...
    }
    

    在init方法中,依次定义了mTextPaint和mPiePaint两个画笔对象。mTextPaint用来绘制PieChart中的标签文本,指示圆点,圆点和标签之间的线段。mPiePaint用来绘制饼状图的各个扇形。

    理解了Android框架为我们提供了Paint和Canvas用来绘制内容之后,那么接下来就分析下如何实现PieChart的内容绘制。
    下面在更具体地提出一个个问题、要完成的功能时,有时会直接对PieChart类引入新的字段、方法、类等来作为实现。

    8. PieView圆的绘制

    根据之前小结《1.1 ViewGroup和View的选择》的讨论,PieChart的圆的绘制是通过另一个类PieView完成的。
    这里PieView类作为PieChart的内部类,方便一些字段的访问。
    PieView绘制的圆是由多个扇形组成的,每个扇形对应一个显示项。这里定义Item类表示此扇形:

    // Item是PieChart的内部类
    private class Item {
        public String mLabel;
        public float mValue;
        public int mColor;
    
        // 在添加显示项的时候,每个显示项会根据所有显示项来计算它的角度范围
        public int mStartAngle;
        public int mEndAngle;
    }
    

    对于PieChart类的使用者,可以通过下面的addItem方法添加任意多个数据项:

    public class PieChart extends ViewGroup {
      ...
      private ArrayList<Item> mData = new ArrayList<Item>();
      private float mTotal = 0.0f;
    
      public int addItem(String label, float value, int color) {
          Item it = new Item();
          it.mLabel = label;
          it.mColor = color;
          it.mValue = value;
    
          mTotal += value;
          mData.add(it);
    
          onDataChanged();
          return mData.size() - 1; // 返回添加的数据在数据集合的索引
      }
    }
    

    可以看到,每个Item有它的颜色、标签和值。每个Item最终展示成一个扇形,扇形的角度大小和它的value在所有Item的value总和的占比成正比。所有扇形从0°开始依次形成一个360°的圆。
    角度的计算很简单,添加新数据项的时候,显示项集合发生变化,方法PieChart.onDataChanged()重新计算了所有Item的startAngle和endAngle:

    public class PieChart extends ViewGroup {
      private void onDataChanged() {    
          int currentAngle = 0;
          for (int i = 0; i < mData.size(); i++) {
              Item it = mData.get(i);
              it.mStartAngle = currentAngle;
              it.mEndAngle = (int) ((float) currentAngle + it.mValue * 360.0f / mTotal);
              currentAngle = it.mEndAngle;
          }
    
          calcCurrentItem();
          onScrollFinished();
      }
    }
    

    得到了所有要显示的扇形Item对象集合mData之后,绘制圆的工作就是从0°开始依次把每个扇形绘制就可以了。
    这里在PieView.onDraw方法中,使用Canvas提供的绘制一个圆弧的方法drawArc来绘制各个扇形:

    /**
      * Internal child class that draws the pie chart onto a separate hardware layer
      * when necessary.
      * PieView作为PieChart的内部类,它在必要的时候(执行动画)在独立的硬件层来绘制内容。
      */
    private class PieView extends View {
      RectF mBounds;
    
      protected void onDraw(Canvas canvas) {
                  super.onDraw(canvas);
          // drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
          // drawArc在给定的oval(椭圆,圆是特殊的椭圆)中从角度startAngle开始绘制角度为sweepAngle的圆弧。
          // 绘制方向为顺时针,圆弧和两条半径组成了每个数据项展示的扇形。
          for (Item it : mData) {
              mPiePaint.setColor(it.mColor);
              canvas.drawArc(mBounds,
                      it.mStartAngle,
                      it.mEndAngle - it.mStartAngle,
                      true, mPiePaint);
          }
      }
    }
    

    画笔对象mPiePaint在每次绘制时扇形时会改变其颜色为要绘制的Item对应扇形的颜色。
    注意,上面drawArc的第一个参数RectF oval:

    * @param oval  The bounds of oval used to define the shape and size
    *              of the arc.
    

    它表示绘制的扇形所在的圆的边界矩形。

    由于PieChart本身绘制标签、指示圆点和连接标签与圆点的线段,它添加PieView对象作为其childView完成绘制圆,PieView.onDraw方法里使用的mBounds是绘制圆用到的边界参数。使用PieChart时,PieView是PieChart的内部类,无法指定它的大小。而是为PieChart指定大小。
    接下来分析PieChart绘制标签和绘制圆所涉及到的边界大小的计算逻辑,以及PieChart作为布局容器,它如何分配给PieView需要的显示区域。

    9. 绘制区域计算

    为了绘制标签和圆,首先需要知道它们的位置和大小,这里就是需要确定PieChart和PieView对象的位置和大小。
    Android UI框架中,所有View在屏幕上占据一个矩形区域,可以用类RectF(RectF holds four float coordinates for a rectangle.)来表示此区域。View最终显示前,它的位置和大小需要确定下来(也就是它的显示区域),可以通过LayoutParams来指定有个View的大小和相对父容器(parent ViewGroup)的位置信息。

    9.1 LayoutParams

    LayoutParams是ViewGroup的静态内部类,它是ViewGroup用到的有关childView布局信息的封装。
    这里布局信息就是childView提供的有关自身大小的数据。
    LayoutParams的内容可以是两种:

    • 具体数值
      layout_width/layout_height设置的是具体的像素值,很明显只能是正数。布局中可以是dp,px等。代码中设置数值就直接是像素,必要的时候需要换算下。
    • 枚举值
      MATCH_PARENT和WRAP_CONTENT两个常量是负数。它们表示当前View对自身所需大小的要求,不是具体的数值,分别表示填充父布局和包裹内容。

    在具体的ViewGroup子类中,可以提供它专有的LayoutParams子类来增加更多有关布局的信息。比如像LinearLayout.LayoutParams中增加了margin属性,可以让childView指定和LinearLayout的间隙。

    一个View的大小可以在代码中使用setLayoutParams指定(默认的addView添加的childView使用的宽高均为LayoutParams.WRAP_CONTENT的LayoutParams),而在布局xml中定义View时,必须使用layout_height和layout_width。

    LayoutParams是指定View布局大小的唯一方式,不像View.setPadding方法那样是为View本身设置有关其显示相关的尺寸信息,它是指定给View的父布局ViewGroup对象的属性,
    而不是针对View本身的属性。最终View的大小和位置是其父布局ViewGroup对象决定的,它使用View提供的LayoutParams参数作为参考,但并不会一定满足childView提供的LayoutParams的布局要求。
    为了明白LayoutParams这样设计的原因,接下来对View从创建到显示的过程做分析。

    9.2 View对象的创建

    整个Activity最终展示的界面是一个由View和ViewGroup对象组成的view hierarchy结构,这里称它为ViewTree(视图树)。可以使用布局xml或完全通过代码创建好所有的View对象。将ViewTree指定给Activity是通过执行Activity的setContentView方法,它有几个重载方法,最完整的是:

    /**
     * Set the activity content to an explicit view.  This view is placed
     * directly into the activity's view hierarchy.  It can itself be a complex
     * view hierarchy.
     *
     * @param view The desired content to display.
     * @param params Layout parameters for the view.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(int)
     */
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }
    

    Android中对屏幕的表示就是一个Window,Activity的内容是通过Window来渲染的。
    在我们为Activity设置内容视图View对象时,它实际上被设置给Window对象,上面Window.setContentView方法
    将传递的View对象作为当前Screen要显示的内容。

    通常,我们所创建的界面内容是由多个View和ViewGroup对象组成的树结构,可以通过hierarchy viewer工具来直观查看:

    ViewTree示例
    对应的布局xml如下: ```xml
  • 相关阅读:
    ServerSocket类的常用方法
    socket互传对象以及IO流的顺序问题
    socket之线程来提高吞吐量
    利用socket传递图片
    socket经典案例-发送数据
    NIO基础方法一
    NIO基础
    java版本的Kafka消息写入与读取
    搭建真正的zookeeper集群
    安装部署Kafka集群
  • 原文地址:https://www.cnblogs.com/everhad/p/5755823.html
Copyright © 2011-2022 走看看