Android 笔记 自定义View,让用户觉得熟悉的控件,才是一个好的控件 (六)

   继承自View或ViewGroup或组合控件 
      通常自定义 View 时,会重写 onDraw() 方法来绘制 View 显示的内容,如果 View 的属性为 wrap_content 属性时,还需重写 onMeasure() 方法来测量 View,主要是针对wrap_content 属性时View 的大小,还可通过自定义 attrs 属性设置新的属性配置。

在 View 中通常有一下比较重要的回调方法:
     a。onFinishInflate():从XML 加载组件后回调
     b。onSizeChanged():组件大小改变时回调
     c。onMeasure():回调该方法进行测量
     d。onLayout():回调该方法来确定显示位置
     e。onTouchEvent():监听到触摸事件时回调

自定义View 的分类

自定义 View 的常见三种方式:
1)继承特定的View,对现有控件进行拓展(比如TextView)
    一般是用于扩展某种已有的View 的功能,比如TextView,这种方法不需要自己支持 wrap_content 和padding 等

2)继承View,重写onDraw 方法
    主要用于实现不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则图形。采用这种方式需要自己支持wrap_content,并且padding 也需要自己处理

3)继承ViewGroup 派生特殊的layout,通过组合来实现新的控件
    主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout 这几种系统布局之外,我们重新定义一种新的布局,当某种效果看起来很像几种View 组合在一起的时候,可以采用这种方法来实现。采用这种方法,需要合适的处理ViewGroup 的测量、布局这两个过程,并同时处理子元素的测量和布局的过程。

4)继承特定的ViewGroup

自定义View 注意事项:
1、让View 支持 wrap_content
        因为直接继承View 或者ViewGroup 的控件,如果不在onMeasure 中对 wrap_content 做特殊处理,那么当外界在布局中使用 wrap_content 时,就无法达到预期的效果,具体情形可查看View 的工作原理中View 的普通View 的 MeasureSpec 的创建规则。

2、如果有必要,让你的View 支持 padding
        因为直接继承View 的控件,如果不在draw 方法中处理 padding,那么padding 属性是无法起作用的。此外直接继承自ViewGroup 的控件需要在 onMeasure 和onLayout 中考虑 padding 和子元素的 margin 对其造成的影响,不然将导致 padding 和子元素的 margin 失效

3、尽量不要再View 中使用 Handler
        因为View 内部本身就提供了post 系列的方法,完全可以替代Handler 的作用,当然除非你明确的要使用Handler 来发送消息

4、View 中如果有线程或者动画,需要及时停止,参考 View#onDetachedFromWindow
        如果有线程或者动画需要停止时,onDetachedFromWindow 是一个很好的时机。当包含此View 的Activity 退出或者当前View 被 remove 时,View 的onDetachedFromWindow会被调用,和此方法对应的是 onAttachedToWindow,当包含此View 的Activity 启动时,View 的onAttachedToWindow 方法会被调用。同时,当View 变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会导致内存泄漏

5、View 带有滑动嵌套情形时,需要处理好滑动冲突
        如果有滑动冲突,则要合适的处理滑动冲突,否则将严重影响View 的效果。



1、对现有控件进行拓展
   1.1 在 onDraw()方法中对原生控件行为进行拓展,通过改变控件的绘制行为来创建自定义View的思路如下所示:
    @Override
    protected void onDraw(Canvas canvas) {
        //在回调父类方法前,实现自己的逻辑,这里即在绘制文本内容前
        super.onDraw(canvas);//调用父类的方法,实现原生控件的功能
        //在回调父类方法前,实现自己的逻辑,这里即在绘制文本内容后
    }
public class MyTextView extends TextView {

    private Paint paint1;
    private Paint paint2;

    public MyTextView(Context context) {
        super(context);
        initView();
    }



    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }
    private void initView() {
        paint1 = new Paint();
        paint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        paint1.setStyle(Paint.Style.FILL);
        paint2 = new Paint();
        paint2.setColor(Color.YELLOW);
        paint2.setStyle(Paint.Style.FILL);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        //在回调父类方法前,实现自己的逻辑,这里即在绘制文本内容前
        canvas.drawRect(0,// 绘制外层矩形
                        0,
                        getMeasuredWidth(),
                        getMeasuredHeight(),
                        paint1);
        // 绘制内层矩形
        canvas.drawRect(10,
                        10,
                        getMeasuredWidth()-20,
                        getMeasuredHeight()-20,
                        paint2);
        canvas.save();//用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、裁剪等操作。
        // 绘制文字前平移10像素
        canvas.translate(50,0);
        // 父类完成的方法,即绘制文本
        super.onDraw(canvas);//调用父类的方法,实现原生控件的功能
        canvas.restore();//用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
        //在回调父类方法前,实现自己的逻辑,这里即在绘制文本内容后
    }
}

1.2给 Paint 增加渐变渲染器
package com.androidheroes.myview.ui;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * @项目名 MyView
 * @包名 com.androidheroes.myview
 * @时间 十二月
 * @描述 TODO
 * Created by Administrator on 2016/12/15.
 */

public class ShineTextView extends TextView {
    private int mViewWidth = 0;
    private int mTranslate = 0;
    private Paint paint;
    private LinearGradient linearGradient;
    private Matrix matrix;

    public ShineTextView(Context context) {
        super(context);
    }

    public ShineTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ShineTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @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) {
                paint = getPaint();
                linearGradient = new LinearGradient(
                        0,//渐变的起始点x坐标;
                        0,//渐变的起始点y坐标
                        mViewWidth,//渐变的终点x坐标;
                        0,//渐变的终点y坐标
                        new int[]{
                                Color.BLUE,
                                0xffffffff,
                                Color.BLUE},//参数colors表示渐变的颜色数组
                        null,//参数positions用来指定颜色数组的相对位置
                        Shader.TileMode.CLAMP);//参数tile表示平铺方式。
                paint.setShader(linearGradient);
                matrix = new Matrix();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (matrix != null) {
            mTranslate += mViewWidth / 5;
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            matrix.setTranslate(mTranslate,0);
            linearGradient.setLocalMatrix(matrix);
            postInvalidateDelayed(100);
        }
    }
}

Shader.TileMode有3种参数可供选择,分别为CLAMP、REPEAT和MIRROR:

CLAMP的作用是如果渲染器超出原始边界范围,则会复制边缘颜色对超出范围的区域进行着色

REPEAT的作用是在横向和纵向上以平铺的形式重复渲染位图

MIRROR的作用是在横向和纵向上以镜像的方式重复渲染位图

首先我们先来onSizeChanged()里面的代码,在这段代码中主要是定义了LinearGradient:

  linearGradient = new LinearGradient(
        0,//渐变的起始点x坐标;
        0,//渐变的起始点y坐标
        mViewWidth,//渐变的终点x坐标;
        0,//渐变的终点y坐标
        new int[]{
                Color.BLUE,
                0xffffffff,
                Color.BLUE},//参数colors表示渐变的颜色数组
        null,//参数positions用来指定颜色数组的相对位置
        Shader.TileMode.CLAMP);//参数tile表示平铺方式。
段代码可以这么理解,它定义了一组渐变的数值是{ 0x33ffffff, 0xffffffff, 0x33ffffff },这个渐变的初始位置是在手机屏幕的(0,0)位置,

我就把LinearGradient这个比作一个长方形,如上图是初始化的位置在手机屏幕的最左边,要运动到屏幕的最右边就需要2*width的长度。
剩下的方法就是很好理解了,这里不再说明了

2.继承View ,重写onDraw方法
package com.example.yhadmin.myview.ui;

/*
 *  @项目名:  MyView 
 *  @包名:    com.example.yhadmin.myview.ui
 *  @文件名:   MyCicler
 *  @创建者:   YHAdmin
 *  @创建时间:  2018/5/9 17:22
 *  @描述:    TODO
 */

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.example.yhadmin.myview.R;


public class MyCircle
        extends View
{

    private int   mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    //默认大小
    private int mWidth  = 200;
    private int mHeigth = 200;

    public MyCircle(Context context) {
        super(context);
        init();
    }

    //
    public MyCircle(Context context, @Nullable AttributeSet attrs)
    {
        this(context, attrs,0);

    }

    public MyCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }



    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width  = getWidth()  ;
        int height = getHeight() ;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width / 2, height / 2, radius, mPaint);
    }
}

        上述代码是一个具有圆形效果的自定义View 的简单实现

布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#ffffff"
    tools:context="com.example.yhadmin.myview.MainActivity">

    <com.example.yhadmin.myview.ui.MyCicler
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000"
        />

</LinearLayout>
        运行效果如图1 所示,符合我们的预期

为其设置 20dp 的margin 后,布局代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#ffffff"
    tools:context="com.example.yhadmin.myview.MainActivity">

    <com.example.yhadmin.myview.ui.MyCicler
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000"
        android:layout_margin="20dp"
        />

</LinearLayout>

 运行效果如图2所示,符合我们的预期


设置20dp padding 后,布局代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#ffffff"
    tools:context="com.example.yhadmin.myview.MainActivity">

    <com.example.yhadmin.myview.ui.MyCicler
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000"
        android:padding="20dp"
        android:layout_margin="20dp"
        />

</LinearLayout>

    运行效果如图3所示,从图片中我们可以发现,padding 根本没有生效


为其宽设置 wrap_content 后,布局代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#ffffff"
    tools:context="com.example.yhadmin.myview.MainActivity">

    <com.example.yhadmin.myview.ui.MyCicler
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:background="#000000"
        android:padding="20dp"
        android:layout_margin="20dp"
        />

</LinearLayout>

     运行效果如图4所示,从图片中我们可以发现,wrap_content根本没有生效,效果相当于match_parent


为解决上面提到的几种问题,我们修改onMeasure和onDraw代码,如下所示:
onMeasure

    //默认大小
    private int mWidth  = 200;
    private int mHeigth = 200;
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeigth);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeigth);
        }

    }

onDraw

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int width  = getWidth() -paddingLeft -paddingRight ;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2, radius, mPaint);
    }

运行效果如图5所示,可以发现 padding 和wrap_content 均已生效


添加自定义属性,在Value 目录下新建attrs.xml 文件,并在其中声明一个 CircleView 属性集合

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyCircle">
        <attr name="circle_cocler" format="color|reference"/>
    </declare-styleable>
</resources>

在View 的构造方法中解析自定义属性的值,并做相应处理,完整代码如下:

package com.example.yhadmin.myview.ui;

/*
 *  @项目名:  MyView 
 *  @包名:    com.example.yhadmin.myview.ui
 *  @文件名:   MyCicler
 *  @创建者:   YHAdmin
 *  @创建时间:  2018/5/9 17:22
 *  @描述:    TODO
 */

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.example.yhadmin.myview.R;


public class MyCircle
        extends View
{

    private int   mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    //默认大小
    private int mWidth  = 200;
    private int mHeigth = 200;

    public MyCircle(Context context) {
        super(context);
        init();
    }

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

    public MyCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCircle);
        mColor = ta.getColor(R.styleable.MyCircle_circle_cocler, Color.RED);

        ta.recycle();
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeigth);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeigth);
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int width  = getWidth() -paddingLeft -paddingRight ;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2, radius, mPaint);
    }
}

布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical"
    tools:context="com.example.yhadmin.myview.MainActivity">

    <com.example.yhadmin.myview.ui.MyCircle
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"
        android:padding="20dp"
        app:circle_cocler="@color/light_green"
        />

</LinearLayout>
运行效果如图6 所示,自定义的属性 circle_color 属性值生效 

3、继承特定的ViewGroup创建组合控件
自定义属性(attrs),需要在 res 资源目录的 values 目录下新建一个 attrs.xml 的属性定义文件,在该文件中可以通过如下代码定义相关属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--TopBar 的自定义属性 reference 表示可引用 dimension表示尺寸 attr 属性名称 format 属性类型 |分隔不同的属性 declare-styleable 指定控件的名称-->
    <declare-styleable name="TopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" /><!--name 属性引用的名称 -->
        <attr name="titleTextColor" 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>

   继承RelativeLayoutTopBar 控件 ,代码如下:

package com.androidheroes.myview.ui;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.androidheroes.myview.R;

/**
 * @项目名 MyView
 * @包名 com.androidheroes.myview.ui
 * @时间 十二月
 * @描述 自定义组合控件
 * Created by Administrator on 2016/12/16.
 */

public class TopBar extends RelativeLayout {
    // 左按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mLeftTextColor;
    private Drawable mLeftBackground;
    private String mLeftText;
    // 右按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mRightTextColor;
    private Drawable mRightBackground;
    private String mRightText;
    // 标题的属性值,即我们在atts.xml文件中定义的属性
    private float mTitleTextSize;
    private int mTitleTextColor;
    private String mTitle;
    //创建新的组件元素
    private Button mLeftButton;
    private Button mRightButton;
    private TextView mTitleView;

    // 布局属性,用来控制组件元素在ViewGroup中的位置
    private LayoutParams mLeftPapams, mRightParams,mTitleParams;

    // 映射传入的接口对象
    private topbarClickListener mListener;

    public TopBar(Context context) {
        super(context);
    }

    public TopBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public TopBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 设置topbar的背景
        setBackgroundColor(0xFFF59563);
        // 通过这个方法,将你在atts.xml中定义的declare-styleable属性集合中
        // 的所有属性的值存储到TypedArray中
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
        // 左按钮的属性值,即我们在atts.xml文件中定义的属性
        mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
        mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
        mLeftText = ta.getString(R.styleable.TopBar_leftText);
        // 右按钮的属性值,即我们在atts.xml文件中定义的属性
        mRightTextColor = ta.getColor(R.styleable.TopBar_rightBackground, 0);
        mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
        mRightText = ta.getString(R.styleable.TopBar_rightText);
        // 标题的属性值,即我们在atts.xml文件中定义的属性
        mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
        mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
        mTitle = ta.getString(R.styleable.TopBar_title);
        //获取完TypedArray的值后,一般要调用ecyle方法释放资源,避免重新创建的时候的错误
        ta.recycle();

        //创建新的组件元素
        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);

        // 为创建的组件元素赋值
        // 值就来源于我们在引用的xml文件中给对应属性的赋值
        mLeftButton.setBackground(mLeftBackground);
        mLeftButton.setText(mLeftText);
        mLeftButton.setTextColor(mLeftTextColor);

        mRightButton.setBackground(mRightBackground);
        mRightButton.setText(mRightText);
        mRightButton.setTextColor(mRightTextColor);

        mTitleView.setText(mTitle);
        mTitleView.setTextSize(mTitleTextSize);
        mTitleView.setTextColor(mTitleTextColor);
        // 为组件元素设置相应的布局元素
        mLeftPapams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mLeftPapams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
        // 添加到ViewGroup
        addView(mLeftButton,mLeftPapams);

        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
        addView(mRightButton,mRightParams);

        mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);

        mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);
        addView(mTitleView,mTitleParams);
        // 按钮的点击事件,不需要具体的实现,
        // 只需调用接口的方法,回调的时候,会有具体的实现
        mRightButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mListener.rightClick();
            }
        });

        mLeftButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mListener.leftClick();
            }
        });
    }
    // 暴露一个方法给调用者来注册接口回调,方法参数即借口对象
    // 通过接口来获得回调者对接口方法的实现
    public void setOnTopbarClickListener(topbarClickListener listener){
        this.mListener = listener;
    }
    // 接口对象,实现回调机制,
    // 在回调方法中通过映射的接口对象调用接口中的方法
    // 而不用去考虑如何实现,具体的实现由调用者去创建
    public interface topbarClickListener{
        // 左按钮点击事件
        void leftClick();
        // 右按钮点击事件
        void rightClick();
    }

    /**
     * 设置按钮的显示与否 通过id区分按钮,flag区分是否显示
     * @param id    控件id
     * @param flag 是否显示
     */
    public void setButtonVisable(int id,boolean flag){
        if (flag){
            if (id==0){
                mLeftButton.setVisibility(VISIBLE);
            }else {
                mRightButton.setVisibility(VISIBLE);
            }
        }else {
            if (id==0){
                mLeftButton.setVisibility(GONE);
            }else {
                mRightButton.setVisibility(GONE);
            }
        }
    }
}
在这里遇到了问题:继承自 RelativeLayout 时,自动重写了 onLayout 方法,导致组合控件无法显示,后删除 onLayout 方法,正常现象,查看   RelativeLayout  代码
发现在 onLayout 方法中调用了子 View 的layout 方法来布局,由于自己未做处理,故无法显示,下面是   RelativeLayout   的onLayout 代码:
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

运用测试

public class TopbarTestActivity extends Activity {

    private TopBar topbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_topbar_test);
        topbar = (TopBar) findViewById(R.id.topbar);
        topbar.setOnTopbarClickListener(new TopBar.topbarClickListener() {
            @Override
            public void leftClick() {
                Toast.makeText(TopbarTestActivity.this,
                        "legt", Toast.LENGTH_SHORT)
                        .show();
            }

            @Override
            public void rightClick() {
                Toast.makeText(TopbarTestActivity.this,
                        "right", Toast.LENGTH_SHORT)
                        .show();
            }
        });
        // 控制topbar上组件的状态
        topbar.setButtonVisable(0,true);
        topbar.setButtonVisable(1,false);
    }
}

引用UI模板 XML代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.androidheroes.myview.activity.TopbarTestActivity">

    <com.androidheroes.myview.ui.TopBar
        android:id="@+id/topbar"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        custom:leftBackground="@drawable/blue_bottom"
        custom:leftText="Back"
        custom:leftTextColor="#fff"
        custom:rightBackground="@drawable/blue_bottom"
        custom:rightText="More"
        custom:rightTextColor="#fff"
        custom:title="自定义标题"
        custom:titleTextColor="#123412"
        custom:titleTextSize="10sp" />

</RelativeLayout>

在引用前,需要制定引用第三方控件的名字空间,即:

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

      这行代码指定了引用的名字空间 xmlns,指定命名空间为 “android” 因此在接下来使用系统属性时,才能使用"android:"来引用Android系统的属性,如要使用自定义的属性,就需要创建自己的名字空间,在Android Studio 中,第三方的控件都使用如下代码来引入名字空间:

 xmlns:custom="http://schemas.android.com/apk/res-auto"
其中 custom 是自己起的名字空间名称,之后在XML中使用自定义的属性时,就可同过这个名字空间来引用,如上所示

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

3、继承 View来实现全新的控件
最自由的实现,需要正确的测量View的尺寸,并且需要手动绘制各种视觉效果
核心步骤:1、尺寸测量 (onMeasure)2、 绘制(onDraw),先分解控件组成,整理思路设计图形
在onMeasure 中获取控件的大小信息,在onDraw 中绘制控件的显示信息

难点在于绘制控件和实现交互,动过onDraw()、onMeasure()来实现绘制逻辑,通过重写 onTouchevent() 等触控事件来实现交互逻辑
代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">
        <attr name="title" format="string"/>
        <attr name="titleTextSize" format="dimension"/>
        <attr name="titleTextColor" format="color"/>
        <attr name="leftTextColor" format="color"/>
        <attr name="leftBackground" format="color|reference"/>
        <attr name="leftText" format="string"/>
        <attr name="rightTextColor" format="color"/>
        <attr name="rightBackground" format="color|reference"/>
        <attr name="rightText" format="string"/>
    </declare-styleable>

    <declare-styleable name="ArcView">
        <attr name="text" format="string"/>
        <attr name="ciclerColor" format="color"/>
        <attr name="arcColor" format="color"/>
    </declare-styleable>
</resources>


package com.example.yhadmin.viewdemo.view;

/*
 *  @项目名:  ViewDemo 
 *  @包名:    com.example.yhadmin.viewdemo.view
 *  @文件名:   ArcView
 *  @创建者:   YHAdmin
 *  @创建时间:  2017/12/7 16:09
 *  @描述:    TODO
 */

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.example.yhadmin.viewdemo.R;

public class ArcView
        extends View
{

    private int   mSpecSizeHeight;
    private int   mSpecSizeWidth;
    private float mCicleXY;
    private float mRadius;
    private Paint mCirclePaint;
    private RectF mArcRectF;
    private Paint mArcPaint;
    private Paint mTextPaint;

    private float mSweepAngle;
    private String mShowText;
    private float mShowTextSize;
    private float mSweepValue = 66;
    private int mArcColor;
    private int mCiclerColor;
    private String mText;

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

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

    public ArcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ArcView);
        mArcColor = ta.getColor(R.styleable.ArcView_arcColor,
                                getResources().getColor(android.R.color.holo_blue_bright));
        mCiclerColor = ta.getColor(R.styleable.ArcView_ciclerColor,
                                   getResources().getColor(android.R.color.holo_blue_bright));
        mText = ta.getString(R.styleable.ArcView_text);
        ta.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mSpecSizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        mSpecSizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        setMeasuredDimension(mSpecSizeWidth, mSpecSizeHeight);
        initView();
    }

    private void initView() {
        float length = 0;
        if (mSpecSizeHeight > mSpecSizeWidth) {
            length = mSpecSizeWidth;
        } else {
            length = mSpecSizeHeight;
        }
        //圆心和半径
        mCicleXY = length / 2;
        mRadius = (float) (length * 0.5 / 2);
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(mCiclerColor);

        mArcRectF = new RectF(
                (float) (length * 0.1),
                (float) (length * 0.1),
                (float) (length * 0.9),
                (float) (length * 0.9));

        mSweepAngle = (mSweepValue / 100f) * 360f;//百分比
        mArcPaint = new Paint();
        mArcPaint.setAntiAlias(true);
        mArcPaint.setColor(mArcColor);
        mArcPaint.setStrokeWidth((float) (length * 0.1));
        mArcPaint.setStyle(Paint.Style.STROKE);

        mShowText = setShowText();
        mShowTextSize = setShowTextSize();
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mShowTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }



    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mCicleXY,mCicleXY,mRadius,mCirclePaint);
        canvas.drawArc(mArcRectF,270, mSweepAngle, false, mArcPaint);
        canvas.drawText(mShowText, 0, mShowText.length(),
                        mCicleXY, mCicleXY + (mShowTextSize / 4), mTextPaint);

    }
    private float setShowTextSize() {
        this.invalidate();
        return 50;
    }

    private String setShowText() {
        this.invalidate();
        return mText;
    }

    public void forceInvalidate() {
        this.invalidate();
    }

    public void setSweepValue(float sweepValue) {
        if (sweepValue != 0) {
            mSweepValue = sweepValue;
        } else {
            mSweepValue = 25;
        }
        this.invalidate();
    }
}


4.继承ViewGroup 派生特殊的layout,通过组合来实现新的控件

public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            scrollBy(-deltaX, 0);
            break;
        }
        case MotionEvent.ACTION_UP: {
            int scrollX = getScrollX();
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();
            if (Math.abs(xVelocity) >= 50) {
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
            }
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

4.继承ViewGroup 派生特殊的layout, 通过组合来实现新的控件

猜你喜欢

转载自blog.csdn.net/daxiong25/article/details/80284726