Android界面设计4--创建自定义的View类

设计良好的类总是相似的。它使用一个好用的接口来封装一个特定的功能(功能),它有效的使用CPU与内存(性能),等等。


1、创建自定义的View类

继承一个View

为了让Android Developer Tools能够识别你的view,你必须至少提供一个constructor,它包含一个Contenx与一个AttributeSet对象作为参数。这个constructor允许layout editor创建并编辑你的view的实例。

自定义View有四个构造函数。

// 如果View是在Java代码里面new的,则调用第一个构造函数
    public CircleView(Context context) {
        super(context);
        Log.e(tag, "1....");

        // 在构造函数里初始化画笔的操作
        init();
    }

    // 如果View是在.xml里声明的,则调用第二个构造函数
    // 自定义属性是从AttributeSet参数传进来的
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.e(tag, "2....");
        // 加载自定义属性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        // 解析集合中的属性circle_color属性
        // 该属性的id:R.styleable.CircleView_circle_color
        // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色)
        // 第二个参数是默认设置颜色(即无指定circle_color情况下使用)
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        // 解析后释放资源
        a.recycle();
        init();
    }

    int mColor;

    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // Viewstyle属性时
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.e(tag, "3....");
        // 加载自定义属性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        // 解析集合中的属性circle_color属性
        // 该属性的id:R.styleable.CircleView_circle_color
        // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色)
        // 第二个参数是默认设置颜色(即无指定circle_color情况下使用)
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        // 解析后释放资源
        a.recycle();
//        init();
    }


    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // Viewstyle属性时
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Log.e(tag, "4....");
    }

定义自定义属性

为了添加一个内置的View到你的UI上,你需要通过XML属性来指定它的样式与行为。良好的自定义views可以通过XML添加和改变样式,为了让你的自定义的view也有如此的行为,你应该:

  • 为你的view在资源标签下定义自设的属性
  • 在你的XML layout中指定属性值
  • 在运行时获取属性值
  • 把获取到的属性值应用在你的view上
为了定义自设的属性,添加 资源到你的项目中。放置于res/values/attrs.xml文件中。下面是一个attrs.xml文件的示例:

<declare-styleable name="CircleView">
    <!--attr标签下设置需要的自定义属性-->
    <!--此处定义了一个设置图形的颜色:circle_color属性,格式是color,代表颜色-->
    <!--格式有很多种,如资源id(reference)等等-->
    <attr name="circle_color" format="color"/>
</declare-styleable>

应用自定义属性

1、资源文件里直接配置

一旦你定义了自设的属性,你可以在layout XML文件中使用它们,就像内置属性一样。唯一不同的是你自设的属性是归属于不同的命名空间。不是属于http://schemas.android.com/apk/res/android的命名空间,它们归属于http://schemas.android.com/apk/res/[your package name]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:CircleView="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.hy.appui.userdefinedview.CircleViewActivity">

    <com.hy.appui.userdefinedview.CircleView
        android:id = "@+id/c1"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000"
        android:padding="20dp"
        CircleView:circle_color = "#BDFCC9"/>

    <Button
        android:id = "@+id/b1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text="change"/>

</LinearLayout>

2、在代码里配置

(1)在自定义view里提供对应的getter/setter接口。

public class CircleView extends View {
    String tag = "060_CircleView";


    // 自定义View有四个构造函数

    // 如果View是在Java代码里面new的,则调用第一个构造函数
    public CircleView(Context context) {
        super(context);
        Log.e(tag, "1....");

        // 在构造函数里初始化画笔的操作
        init();
    }

    // 如果View是在.xml里声明的,则调用第二个构造函数
    // 自定义属性是从AttributeSet参数传进来的
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.e(tag, "2....");
        // 加载自定义属性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        // 解析集合中的属性circle_color属性
        // 该属性的id:R.styleable.CircleView_circle_color
        // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色)
        // 第二个参数是默认设置颜色(即无指定circle_color情况下使用)
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        // 解析后释放资源
        a.recycle();
        init();
    }

    int mColor;

    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // Viewstyle属性时
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.e(tag, "3....");
        // 加载自定义属性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        // 解析集合中的属性circle_color属性
        // 该属性的id:R.styleable.CircleView_circle_color
        // 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色)
        // 第二个参数是默认设置颜色(即无指定circle_color情况下使用)
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        // 解析后释放资源
        a.recycle();
//        init();
    }


    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // Viewstyle属性时
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        Log.e(tag, "4....");
    }

    // 设置画笔变量
    Paint mPaint1;
    // 画笔初始化
    private void init() {
        // 创建画笔
        mPaint1 = new Paint();
        // 设置画笔颜色为蓝色
        mPaint1.setColor(mColor);
        // 设置画笔宽度为10px
        mPaint1.setStrokeWidth(5f);
        //设置画笔模式为填充
        mPaint1.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);

        Log.e(tag, "onMeasure >>>>>>>>>>>>widthMeasureSpec : " + widthMeasureSpec + " , " + " heightMeasureSpec : " + heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed,left,top,right,bottom);

        Log.e(tag, "onLayout >>>>>>>>>>>>changed : " + changed
                + " , " + " left" + left
                + " , " + " top" + top
                + " , " + " right" + right
                + " , " + " bottom" + bottom);
    }

    // 复写onDraw()进行绘制
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(tag, "onDraw");

        init();

        // 获取传入的padding        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();

        // 获取绘制内容的高度和宽度(考虑了四个方向的padding值)
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;

        // 设置圆的半径 = ,高最小值的2分之1
        int r = Math.min(width, height) / 2;

        // 画出圆(蓝色)
        // 圆心 = 控件的中央,半径 = ,高最小值的2分之1
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1);
    }

    public int getCircleColor(){
        return mColor;
    }
    public void setCircleColor(int color){
        mColor = color;
        this.postInvalidate();

    }
}

2、实现自定义View的绘制

Override onMeasure()

作用:测量子控件的大小,也就是width和heighth。

onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果.

/**
 * specMode一共有三种类型,如下所示:
 1. EXACTLY
 表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,简单的说(当设置widthheightmatch_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
 2. AT_MOST
 表示子视图最多只能是specSize中指定的大小。(当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
 3. UNSPECIFIED
 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
 */


int WRAP_WIDTH = 300;  //设置width的上限
int WRAP_HEIGHT = 300; //设置heighth的上限
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,heightMeasureSpec);

    int width = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    Log.e(tag, "onMeasure >>>>>>>>>>>>width : " + width
            + " , " + " widthMode : " + widthMode
            + " , " + " height : " + height
            + " , " + " heightMode : " + heightMode);

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(WRAP_WIDTH, WRAP_HEIGHT);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(WRAP_WIDTH, height);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width, WRAP_HEIGHT);
    }
}

疑问疑问疑问疑问疑问疑问疑问疑问疑问疑问疑问

Activity中需要view的宽高时,onCreateonStartonResume中都是无法获取的。这是由于view的生命周期和Activity的生命周期不是同步的。解决方法如下:

Activity中在onWindowFocusChanged 中获取。这时View已经初始化完了,可以获取宽高。当Activity窗口获得焦点和失去焦点时均会被调用,因此该函数会被调用多次。


举个栗子

1、

public class CircleViewActivity extends Activity {
    String tag = "060_CircleViewActivity";

    CircleView c1;
    Button b1;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_panelview);

        c1 = (CircleView)findViewById(R.id.c1);

        b1 = (Button) findViewById(R.id.b1);
        b1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                c1.setCircleColor(Color.YELLOW);
            }
        });
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = c1.getWidth();
            int height = c1.getHeight();
            Log.e(tag, "width: " + width);
            Log.e(tag, "height: " + height);
            Log.e(tag, "measuredWidth: " + c1.getMeasuredWidth());
            Log.e(tag, "measuredHeight: " + c1.getMeasuredHeight());
        }
    }
}

activity_panelview.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:CircleView="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.hy.appui.userdefinedview.CircleViewActivity">

    <com.hy.appui.userdefinedview.CircleView
        android:id = "@+id/c1"
        android:layout_width = "match_parent"
        android:layout_height="match_parent"
        android:background="#000000"
        CircleView:circle_color = "#BDFCC9"/>

    <Button
        android:id = "@+id/b1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text="change"/>

</LinearLayout>

1、设置为match_parent

android:layout_width = "match_parent"
android:layout_height="match_parent"

测量出控件大小:

06-28 04:25:37.851 514-514/com.zxcn.test E/060_CircleViewActivity: width: 680

06-28 04:25:37.852 514-514/com.zxcn.test E/060_CircleViewActivity: height: 1120
06-28 04:25:37.852 514-514/com.zxcn.test E/060_CircleViewActivity: measuredWidth: 680

06-28 04:25:37.852 514-514/com.zxcn.test E/060_CircleViewActivity: measuredHeight: 1120

2、设置为wrap_content

<com.hy.appui.userdefinedview.CircleView
    android:id = "@+id/c1"
    android:layout_width = "wrap_content"
    android:layout_height="wrap_content"
    android:background="#000000"
    CircleView:circle_color = "#BDFCC9"/>

06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: width: 300
06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: height: 300
06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: measuredWidth: 300

06-28 04:32:08.883 1269-1269/com.zxcn.test E/060_CircleViewActivity: measuredHeight: 300

3、设置为精确大小


Override onLayout()

设置view的左端、顶端、右端和底端距父控件坐标原点的距离

 layout(0, 100, childWidth + 0, childHeigth+100);//设置子控件的位置,需要首先获得子控件的大小

Override onDraw()

1、创建绘图对象

Canvas提供绘制矩形的方法,Paint定义是否使用颜色填充。简单来说:Canvas定义你在屏幕上画的图形,而Paint定义颜色,样式,字体,

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Log.e(tag, "onDraw");

    init();

    // 获取传入的padding    final int paddingLeft = getPaddingLeft();
    final int paddingRight = getPaddingRight();
    final int paddingTop = getPaddingTop();
    final int paddingBottom = getPaddingBottom();

    // 获取绘制内容的高度和宽度(考虑了四个方向的padding值)
    int width = getWidth() - paddingLeft - paddingRight;
    int height = getHeight() - paddingTop - paddingBottom;

    // 设置圆的半径 = ,高最小值的2分之1
    int r = Math.min(width, height) / 2;

    // 画出圆(蓝色)
    // 圆心 = 控件的中央,半径 = ,高最小值的2分之1
    canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1);
}
// 设置画笔变量
Paint mPaint1;
// 画笔初始化
private void init() {
    // 创建画笔
    mPaint1 = new Paint();
    // 设置画笔颜色为蓝色
    mPaint1.setColor(mColor);
    // 设置画笔宽度为10px
    mPaint1.setStrokeWidth(5f);
    //设置画笔模式为填充
    mPaint1.setStyle(Paint.Style.FILL);
}


3、使得View可交互

处理输入的手势

疑问实现随手指移动控件的效果。

private int lastX;
private int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
    // 获取当前触摸的绝对坐标
    int rawX = (int) event.getRawX();
    int rawY = (int) event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 上一次离开时的坐标
            lastX = rawX;
            lastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            // 两次的偏移量
            int offsetX = rawX - lastX;
            int offsetY = rawY - lastY;
            moveView(offsetX, offsetY);
            // 不断修改上次移动完成后坐标
            lastX = rawX;
            lastY = rawY;
            break;
        default:
            break;
    }
    return true;
}

//跟随手指移动
private void moveView(int offsetX, int offsetY) {
    // 方法五
    ((View) getParent()).scrollBy(-offsetX, -offsetY);
}


4、优化自定义View

Do Less, Less Frequently

1、先从onDraw开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致GC,从而导致卡顿。

2、尽量减少调用invaildate()的次数。

//重写调用onDraw()
this.postInvalidate();

猜你喜欢

转载自blog.csdn.net/haobobo710/article/details/80842651