自定义View

来源:互联网 发布:csp是什么软件 编辑:程序博客网 时间:2024/06/07 11:02

        Android给我们提供了丰富的组件来创建Ui效果,同时也提供了非常方便的拖着方法。通过继承Android的系统组件,我们可以非常方便地拓展现有功能,在系统组件的基础上创建新的功能,甚至可以自己自定义一个控件,实现Android系统控件所没有的功能。自定义控件作为Android做一个非常重要的功能,一直以来都被初学者认为是代表高手的象征。其实,自定义View并没有想象中的那么难,与其说是在自定义一个View,不如说是在设计一个图形,只要站在设计者的角度上,才可以更好地创建自定义View。我们不能机械地记忆所有绘图的API,而是要让这些API为你所用,结合现实中的绘图方法,甚至是PhotoShop的技巧,才能设计出更好的自定义View。

        适当的使用自定义View,可以丰富应用程序的体验效果,但滥用自定义View则会带来适得其反的效果。一个让用户觉得熟悉的控件,才是一个好的控件。如果一味追求炫酷的效果而创建自定义View,则会让用户觉得华而不实。而且,在系统原生控件可以实现功能的基础上,系统也提供了主题、图片资源、各种风格来创建丰富的UI。这些控件都是经过了Android一代代版本迭代后的产物。即使这样,在如今的版本中,依然还存在不少Bug,更不要提我们自定义的View了。特别是现在AndroidRom的多样性,导致Android的适配变得越来越复杂,很难保证自定义View在其他手机上也能达到你想要的效果。

      当然,了解Android系统自定义View的过程,可以帮助我们了解系统的绘图机制。同时,,在适当的情况下,也可以通过自定义View来帮我们创建更加灵活的布局。

      在自定义View时,我们通常去重写onDraw()方法来绘制View的显示内容。如果该View还需要使用wrap_content熟悉,那么还必须重写onMeasure()方法。另外,通过自定义attrs属性,还可以设置新的属性配置值。

      在View中通常有以下一些比较重要的回掉方法。

  • onFinishInflate():从XML加载组件后回调。
  • onSizeChanged():组件大小改变时回调。
  • onMeasure():回调该方法来进行测量。
  • onLayout():回调该方法来确定显示的位置。
  • onTouchEvent():监听触摸时间时回调。

      当然,创建自定义View的时候,并不需要重写所有的方法,只需要重写特定条件的回调方法即可。这也是Android控件架构灵活性的体现。

1.对现有的控件进行拓展

      这是一个非常重要的自定义View方法,它可以在原生控件的基础上进行拓展,增加新的功能、修改UI等。一般来说,我们可以在onDraw()方法中对原生控件行为进行拓展。

      下面以一个Text View为例,来看看如何使用拓展原生控件的方法创建新的控件。比如想让一个TextView的背景更加丰富,给其多绘制几层背景。

       原生的TextView使用onDraw()方法绘制要显示的文字。当继承了系统的Text View之后,如果不重写其onDraw()方法,则不会修改TextView的任何效果。可以认为在自定义的TextView中调用调用其onDraw()方法来绘制了显示的文字。

      调用super.onDraw(canvas)方法来实现原生控件的功能,但在调用super.onDraw()方法之前和之后,我们都可以实现自己的逻辑。

      以上就是通过改变控件的绘制行为创建自定义View的思路。有 了上面的分析,我们就可以很轻松的实现上图所展示的效果了。我们在构造方法中完成必要对象的初始化工作,如初始化画笔等,代码如下所示。

    //初始化相关变量    private void init(){        paint1 = new Paint();        paint1.setStyle(Paint.Style.FILL);        paint1.setColor(Color.BLUE);        paint2 = new Paint();        paint2.setStyle(Paint.Style.FILL);        paint2.setColor(Color.YELLOW);    }

       而代码中最重要的部分则是在onDraw()方法中,为了改变原生的绘制行为,在系统调用super.onDraw()方法钱,也就是在绘制文字之下,绘制两个大小不同的矩形,形成一个重叠效果,再让系统调用super.onDraw()方法执行绘制文字的工作。这样,我们就通过改变控件绘制行为创建一个新的控件,代码如下

//画外层        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),paint1);        //画内层        canvas.drawRect(10,10,getMeasuredWidth()-10,getMeasuredHeight()-10,paint2);        canvas.save();        //移动文字        canvas.translate(10,0);        super.onDraw(canvas);        canvas.restore();

      

      下面再来看一个稍微复杂一点TextView。在前面一个实例中,我们直接使用了Canvas对象来进行图像的绘制,然后利用Android的绘图机制,可以绘制出更加复杂丰富的图形。比如可以利用LinearGradient Shader和Matrix来实现一个动态的文字闪动效果。

      想要实现这一效果,可以充分利用Android中Paint对象的Shader渲染器。通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制要显示的文字。首先,在onSizeChanged()方法中进行一些对象的初始化工作,并根据View的宽设置一个LinearGradient渐变渲染器,代码如下:

    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        if(mViewWidth == 0){            mViewWidth = getMeasuredWidth();            if(mViewWidth > 0){                //获取当前绘制TextView的Paint                mPaint = getPaint();                mLinearGradient = new LinearGradient(0,0,mViewWidth,0,new int[]{Color.BLUE,0xfff,Color.BLUE},null, Shader.TileMode.CLAMP);                //给Paint对象设置原生TextView没有的LinearGradient属性                mPaint.setShader(mLinearGradient);                mGradientMatrix = new Matrix();            }        }    }

      其中最关键的时就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给这个Paint对象设置原生TextView没有的LinearGradient属性。最后在onDraw()方法中,通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态闪动效果,代码如下所示

@Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        if (mGradientMatrix != null){            mTranslate += mViewWidth/5;            if (mTranslate > 2*mViewWidth){                mTranslate = -mViewWidth;            }            mGradientMatrix.setTranslate(mTranslate,0);            //通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态的闪动效果            mLinearGradient.setLocalMatrix(mGradientMatrix);            postInvalidateDelayed(100);        }    }


2.创建复合控件

      创建符合控件可以很好的创建具有重用功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再给她添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给他指定一些可配置的属性,让它具有更强的拓展性。下面就以一个TopBar为例,讲解如何创建复合控件。

      我们知道为了应用程序风格的统一,很多应用程序都有一些共通的UI界面,比如TopBar这样的标题栏。

      通常情况下,这些界面都会被抽象出来,形成一个共通的UI组件。所有需要添加标题栏的界面都会引用这样一个TopBar,而不是每个界面都在布局文件中写这样一个TopBar,这样不仅可以提高界面的复用率,更能在需要修改UI时,做到快速修改,而不需要对每个页面的标题看都进行修改。

      下面我们就来看看如何创建一个这样的UI模板。首先,模板应该具有通用性与可定制性。也就是说,我们需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息,而不是所有的模板都一样,那样就是去了模板的意义。

  2.1定义属性

      为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义的相应的属性即可。

    

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="TopBar">        <attr name="title" format="string"/>        <attr name="titleSize" format="dimension"/>        <attr name="titleColor" format="color"/>        <attr name="leftTextColor" format="color"/>        <attr name="leftBackground" format="reference|color"/>        <attr name="leftText" format="string"/>        <attr name="rightTextColor" format="color"/>        <attr name="rightBackground" format="reference|color"/>        <attr name="rightText" format="string"/>    </declare-styleable></resources>

       我们在代码中通过<declare-styleable>标签声明了使用自定义属性,并通过name属性来确定引用的名称。最后,通过<attr>标签来声明具体的自定义属性,比如在这里定义了文字颜色、背景、字体等属性,通过format标签来指定属性的类型。这里需要注意的就是,有些属性可以是颜色属性,也可以是引用属性。比如按钮的背景,可以把他指定为具体的颜色,也可以把他指定为一张照片,所以使用"|"来分割不同的属性----"reference|color"。

      在确定好属性后,就可以创建一个自定义控件----TopBar,并让他继承ViewGroup,从而组合一些需要的控件。这里为了简单,我们继承RelativeLayout。在构造方法中通过如下所示的代码来获取XML布局文件中自定义的那些属性,即与我们使用系统提供的属性一样。

     

TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.Toolbar);

      
     系统提供了TypedArray这样的数据结构来获取自定义属性集、后面引用的styleable的TopBar,就是我们在Xml中通过<declare-styleable name="TopBar">指定的name名。接下来,通过TypedArray对象的getString()方法、getColor()方法,就可以获取这些定义的属性值,代码如下所示。

        //通过这个方法将你在atts.xml中定义的declare-styleable的所有的属性值存储到TypedArray中        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ToolBar);        //从TypedArray中取出对于的值来为要设置的属性赋值        mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);        mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);        mLeftText = ta.getString(R.styleable.TopBar_leftText);        mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor,0);        mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);        mRightText = ta.getString(R.styleable.TopBar_rightText);        mTitle = ta.getString(R.styleable.TopBar_title);        mTitleColor = ta.getColor(R.styleable.TopBar_titleColor,0);        mTitleSize = ta.getDimension(R.styleable.TopBar_titleSize,10);                //获取完TypeArray的值后,一般调用recycle()方法来避免重新穿件的时候的错误。        ta.recycle();

      这里需要注意的是,当获取完所有的属性值后,需要调用TypedArray的recycle方法来完成资源的回收。

   2.2组合控件

      接下来就可以开始组合控件了。UI模板TopBar实际上由三个控件组成,即左边的点击按钮mLeftButton,右边的点击按钮mRightButton和中间的变体栏mTitleView。通过动态添加控件的方式,使用addView()方法将这三个控件加入到定义的TopBar模板中,并给他们设置我们前面所获取的到的具体的属性值,比如标题的文字颜色、大小等,代码如下所示。

mLeftButton = new Button(context);        mRightButton = new Button(context);        mTextView = new TextView(context);        //为创建的元素赋值,值来源与我们在引用xml文件中给对应属性的赋值        mLeftButton.setText(mLeftText);        mLeftButton.setTextColor(mLeftTextColor);        mLeftButton.setBackground(mLeftBackground);        mRightButton.setText(mRightText);        mRightButton.setTextColor(mRightTextColor);        mRightButton.setBackground(mRightBackground);        mTextView.setText(mTitle);        mTextView.setTextColor(mTitleColor);        mTextView.setTextSize(mTitleSize);        //为元素设置相应的布局元素        mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);        //添加到ViewGroup        addView(mLeftButton,mLeftParams);        //为元素设置相应的布局元素        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);        //添加到ViewGroup        addView(mRightButton,mRightParams);        mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);        mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);        addView(mTextView, mTitleParams);        mLeftButton.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                if(mListener !=null){                    mListener.leftClick();                }            }        });        mRightButton.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                if(mListener !=null){                    mListener.rightClick();                }            }        });

      那么如何来给者两个左、右按钮设计点击事件呢?既然是UI模板,那么每个调用者所需要这些按钮能够实现的功能是不一样的。因此,不能直接在UI模板中添加具体的实现逻辑,只能通过接口回调的思想,将具体的实现逻辑交给调用者,实现过程如下所示。

  •       定义接口

      在UI模板类中定义个左右按钮点击的接口,并创建两个方法,分别用于左边按钮的点击和右边按钮的点击,代码如下所示。

public interface TopBarClickListener{        //左按钮点击事件        void leftClick();        //右按钮点击事件        void rightClick();    }
  •       暴露给接口调用者

      在模板方法中,为左右按钮增加点击事件,但不去实现具体的逻辑,而是调用接口中想应的点击方法,代码如下所示。

        mLeftButton.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                if(mListener !=null){                    mListener.leftClick();                }            }        });        mRightButton.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                if(mListener !=null){                    mListener.rightClick();                }            }        });        //暴露一个方法给调用者来注册接口回调。通过接口来获取回调者对接口方法的实现    public void setOnTopBarClickListener(TopBarClickListener listener){        mListener = listener;    }
  •       实现接口回调

      在调用者的代码中,调用者需要实现一个这样的接口,并完成接口中的方法,确定具体的实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。通常情况下,可以使用内名内部类的形式来实现接口中的方法,代码如下所示

topBar.setOnTopBarClickListener(new TopBar.TopBarClickListener() {            @Override            public void leftClick() {                //具体逻辑实现,这里用Toast显示                Toast.makeText(MainActivity.this, "left",Toast.LENGTH_SHORT).show();            }            @Override            public void rightClick() {                //具体逻辑实现,这里用Toast显示                Toast.makeText(MainActivity.this, "right",Toast.LENGTH_SHORT).show();            }        });

      除了通过接口回调的方式来实现动态的控制UI模板,同样可以使用公共方法来动态的修改UI模板中的UI,这样就进一步提高了模板的可定制性,代码如下所示。

      

/**     * 控制按钮的显示     * @param flag--显示、不现实     * @param id--0 left , 1 right     */    public void setButtonVisable(boolean flag, int id){        if(flag){            //显示某个按钮            if(id == 0){                mLeftButton.setVisibility(VISIBLE);            }else{                mRightButton.setVisibility(VISIBLE);            }        } else {            //不显示某个按钮            if(id == 0){                mLeftButton.setVisibility(GONE);            }else{                mRightButton.setVisibility(GONE);            }        }    }

      通过如上代码,当调用者通过TopBar对象调用这个方法后,根据参数,调用者就可以动态的控制按钮的显示,代码如下所示:

//控制topbar上组件的状态mToolBar.setButtonVisable(true,0);mTopBar.setButtonVisable(false,1);

      2.3.引用UI模板

        最后一步,自然是在需要使用的地方引用UI模板,在引用前,需要指定第三方控件的名字控件。在布局文件中可以看到如下一行的代码。

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

      这行代码就是在指定引用的名字控件xmlns即xml namespace。这里指定了名字控件为android,因为在接下来使用系统属性的时候,才可以使用“android:”来引用Android的系统属性。同样地,如果要使用自定i的属性,那么就需要创建自己的名字控件,在Android Studio中,第三方的控件都使用如下代码来涌入控件名字。

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

      这里我们将引入的第三方控件的名字空间取名为custom,之后在XML文件中使用自定义的属性时,就可以通过这个名字控件来引用,代码如下所示。

<test.chenj.study_3_1.TopBar        android:id="@+id/topBar"        android:layout_width="match_parent"        android:layout_height="40dp"        custom:leftBackground="@color/colorPrimaryDark"        custom:leftText="Back"        custom:leftTextColor="#fff"        custom:rightBackground="@color/colorPrimary"        custom:rightText="More"        custom:rightTextColor="#fff"        custom:title="自定义标题"        custom:titleColor="#123412"        custom:titleSize="10sp"/>

使用自定义的View与系统原生的Vi最大的区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。

      在更进一步,如果将这个UI模板写到一个布局文件中,代码如下所示。

<?xml version="1.0" encoding="utf-8"?><test.chenj.study_3_1.TopBar xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:custom="http://schemas.android.com/apk/res-auto"    android:id="@+id/topBar"    android:layout_width="match_parent"    android:layout_height="40dp"    custom:leftBackground="@color/colorPrimaryDark"    custom:leftText="Back"    custom:leftTextColor="#fff"    custom:rightBackground="@color/colorPrimary"    custom:rightText="More"    custom:rightTextColor="#fff"    custom:title="自定义标题"    custom:titleColor="#123412"    custom:titleSize="10sp"/>

      通过上图所示的代码,我们就可以在其他的布局文件中直接通过<include>标签来引用这个UI模板View,代码如下。

<include layout="@layout/topbar"/>

   3.重写View来实现全新的控件

      当Android系统原生的控件无法满足我们的需求时,我们就可以创建完全自定义的View来实现需要的功能。创建一个自定义View,难点在于绘制控件和实现交互,这也是评价一个自定义View的优劣的标准之一。通常需要继承View类,并重写它的onDraw()、onMeasure()、等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然,我们还可以像实现组合控件方式那样,通过引入自定义属性,丰富自定义View的可定制型。

      下面就通过几个实例,了解下如何创建一个自定义View,不过为了让程序尽可能简单,就不去自定义属性了。

      3.1 弧线展示图

      在PPT的很多模板中,都有一张比例图。这个比例图可以分成清楚地展示一个项目所占的比例,简洁明了。因此,实现这样一个自定义View用在我们的程序中,可以让整个程序实现比较清晰的数据展示效果。那么该如何创建一个这样的自定义View呢?很明显,这个自定义View其实分成3个部分,分别时中间的圆形、中间的显示文字和外圈的弧线。既然有了这样的思路,只要在onDraw()方法中一个个去绘制就可以 了。这里为了简单,父布局的宽度。

      在构造函数终先初始化画笔.

mCiclePaint.setStyle(Paint.Style.FILL);        mCiclePaint.setColor(0xffff9800);        mArcPaint = new Paint();        mArcPaint.setColor(0xffff9800);        mArcPaint.setStrokeWidth(100);        mArcPaint.setStyle(Paint.Style.STROKE);        mTextPaint = new Paint();        mTextSize = 50;        mTextPaint.setTextSize(mTextSize);        mTextPaint.setTextAlign(Paint.Align.CENTER);

       在onMeasure终获得宽度,然后在onDraw终画出3个图形

   
@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);        mWidthSize = MeasureSpec.getSize(widthMeasureSpec);        mCircleX = mWidthSize/2;//圆中心的x坐标为宽度的一半        mCircleY = mWidthSize/2;//圆中心的y坐标为宽度的一半        mRadio = mWidthSize/4;        setMeasuredDimension(mWidthSize,mWidthSize);    }

@Override    protected void onDraw(Canvas canvas) {        //画圆        canvas.drawCircle(mCircleX,mCircleY, mRadio,mCiclePaint);        //画弧线        RectF arcRectF = new RectF(mWidthSize/10, mWidthSize/10, mWidthSize*9/10,mWidthSize*9/10);        canvas.drawArc(arcRectF, 270, mSwipValue, false, mArcPaint);        //画文字,文字的中心在基线上,想要文字居中,则还需要向下移动1/4的高度        canvas.drawText(mShowText, 0, mShowText.length(), mCircleX, mCircleY, mTextPaint);    }

       效果如下图所示

 


   3.2音频条图形

      想实现类似在PC上某些音乐播放器上根据音频音量大小显示的音频条形图。

      如果要实现一个静态音频条形图,相信大家应该很快能找到思路,也就是绘制一个个矩形,每个矩形之间稍微偏移一点巨鹿即可。如下代码展示了一种计算坐标的方法。

@Override    protected void onDraw(Canvas canvas) {        //计算出每个矩形的宽度        mRectwidth = (mWidthSize - (mRectCount+1)*offset)/mRectCount;        //画出几个矩形        for(int i=0;i<mRectCount;i++){            float currentHeight = mRectWidth;            //画出每个矩形            canvas.drawRect(offset*(i+1)+mRectwidth*i, currentHeight, (mRectwidth+offset)*(i+1), mRectHeight, mPaint);        }        //每隔一段时间就进行重绘        postInvalidateDelayed(300);    }

      上述代码终,我们通过循环创建这些小的矩形,其中currentHeight就是每个小矩形的高,通过横坐标的不断偏移,就绘制出了这些静态的小矩形.下面我们在让找些小矩形的高度进行随机变化,通过Math.random()方法来随机改变这些高度值,并赋值给currentHeight,代码如下所示

currentHeight = (float)(mRectHeight * Math.random());

       这样我们就完成了静态效果的绘制,那么如何实现动态效果呢?其实非常简单,只要在onDraw()方法终再去调用invalidate()方法通知View进行重绘就可以了。不过,在这里不需要每一次绘制完成新的矩形就通知View进行重绘,这样会因为刷新速度太快而影响效果,因此,使用如下代码进行View的延迟重绘,代码如下所示。

postInvalidateDelayed(300);

      这样每个300ms通过View进行重绘,就可以得到一个比较好的视觉效果了。最后为了让自定义View更加好看,可以在绘制小矩形的时候,给绘制的Paint对象增加一个LinearGradient渐变效果,这样不同高度的矩形就会由不同颜色的渐变效果。更加能够模拟音频条的风格。代码如下

    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);       LinearGradient linearGradient = new LinearGradient(0,0, mRectwidth, mRectHeight,Color.YELLOW, Color.BLUE, Shader.TileMode.CLAMP);       mPaint.setShader(linearGradient);    }

      从这个例子终,我们可以知道,在创建自定义View的时候,需要一步一步来,从一个基本的效果开始,慢慢的增加功能,绘制更复杂的效果。不论是多么复杂的自定义View,它一定是慢慢迭代起来的功能,所以不要觉得自定义View由多难。千里之行始于足下,只要开始做,慢慢的就能越来越熟练。

      最后附上一张效果图







      

      









      









      

      
















 
原创粉丝点击