自定义View

2019-03-17  本文已影响0人  君袅

一.xml的实质

  1. xml不是必须,布局可以代码写
  2. xml是为了开发者开发布局便利,谷歌给开发者开发糖
  3. xml最终还是会转成代码执行

二.View和ViewGroup

1.View
1.1 View是用户界面一个组件(控件)
1.2 View是一个矩形
1.3.View的职责是绘制和事件处理
2.ViewGroup
2.1 ViewGroup是一个特殊的View,它继承自View
2.2 ViewGroup可包含其他View(孩子)
2.3.ViewGroup常用layout的基类
2.4 ViewGroup定义了孩子的布局参数(带layout_前缀的属性)
3.View和ViewGroup的关系
3.1继承关系


image.png

3.2组合关系


image.png

一.什么是自定义控件?

  1. 原生控件:SDK已经有,Google提供
  2. 自定义控件: 开发者自己开发的控件,分三种
    a. 组合式控件:将现有控件进行组合,实现功能更加强大控件;
    b. 继承现有控件: 对其控件的功能进行拓展;
    c. 重写View实现全新的控件.
    二.为什么要自定义View?
    1.原有控件无法满足我们的需求,所以需要自己实现想要的效果.

三.组合式控件 下拉选择框
模块化思想,提高代码复用率
1.功能分析:
a. 点击箭头,弹出下拉列表 (Popupwindow + ListView)
b. 点击列表选项,在编辑框中显示内容
2.实现步骤:
a.继承布局,重写构造;
b.创建布局xml,加入到自定义控件里面;
3.实现对应的功能.
1.xml布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <EditText
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:id="@+id/et"
        />
    <ImageView
        android:id="@+id/xiala"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_alignParentRight="true"
        android:src="@mipmap/xiala"
        />
</RelativeLayout>

2.创建继承RelativeLayout的类

/**
 * Created by asus on 2019/3/15.
 * 1.继承布局,重写构造;
 * 2.创建布局xml,加入到自定义控件里面;
 * 3.实现对应的功能.
 */

public class Spinner extends RelativeLayout {

    private EditText mEt;
    private ImageView mIv;
    private PopupWindow mPopupWindow;
    private ArrayList<String> mData;

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

    //必要的
    public Spinner(Context context, AttributeSet attrs) {
        super(context, attrs);
        initData();
        init(context);
        initListener();
    }

    private void initData() {
        mData = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            mData.add("秋裤:"+i);
        }
    }

    private void initListener() {
        mIv.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                pop();
            }
        });
    }

    //弹出popwindow
    private void pop() {
        if (mPopupWindow == null){
            ListView listView = new ListView(getContext());
            listView.setBackgroundResource(R.drawable.listview_background);
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                    getContext(),android.R.layout.simple_list_item_1,mData);
            listView.setAdapter(adapter);
            listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    String s = mData.get(position);
                    mEt.setText(s);
                    //将光标移动到内容后面
                    mEt.setSelection(s.length());
                    mPopupWindow.dismiss();
                }
            });
            mPopupWindow = new PopupWindow(listView, mEt.getWidth(), 600);
            //点击外部消失
            mPopupWindow.setBackgroundDrawable(new ColorDrawable());
            mPopupWindow.setOutsideTouchable(true);
        }
        //在某个view下方显示
        mPopupWindow.showAsDropDown(mEt,0,0);
    }

    private void init(Context context) {
        View inflate = LayoutInflater.from(context).inflate(R.layout.spinner, null);
        //2.创建布局xml,加入到自定义控件里面;
        addView(inflate);
        mEt = inflate.findViewById(R.id.et);
        mIv = inflate.findViewById(R.id.iv);
    }
}

3.最后这个类就是你自定义的View 在activity中调用

二.小球跟随

public class BallView extends View {

    private Bitmap mBall;
    private Paint mPaint;
    private float mTouchX = 0;
    private float mTouchY = 0;

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

    public BallView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mBall = BitmapFactory.decodeResource(getResources(), R.drawable.ball);
        mPaint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBall,mTouchX,mTouchY,mPaint);
    }

    /**
     * 手指摸哪里,小球跟着去哪里
     * 触摸事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mTouchX = event.getX();
        mTouchY = event.getY();
        //重新绘制
        invalidate();
        //true,消费事件,false,不消费,默认false
        return true;
    }
}

4.3View的绘制(draw)
draw方法绘制要遵循一定的顺序:
1.画背景
2,5.画边缘
3.画自身: ondraw方法
4.画子View: dispatchDraw方法
6.画滚动条
draw绘制流程:


image.png

draw()
draw是由ViewRoot的performTraversals方法发起,它将调用DecorView的draw方法,并把成员变量canvas传给给draw方法。而在后面draw遍历中,传递的都是同一个canvas。所以android的绘制是同一个window中的所有View都绘制在同一个画布上。等绘制完成,将会通知WMS把canvas上的内容绘制到屏幕上。自定义View时一般不重写该方法。

onDraw()
a. View用来绘制自身的实现方法,如果我们想要自定义View,通常需要重载该方法。
b. 比如TextView中在该方法中绘制文字、光标和CompoundDrawable,
ImageView中相对简单,只是绘制了图片
五.绘制实战

public class MyView extends View {
    private static final String TAG = "MyView";
    private int mStartX;
    private int mStartY;
    private int mEndX;
    private int mEndY;
    private Paint mPaint;
    private int mCenterX;
    private int mCenterY;
    private int mRadius;
    private Paint mCirclePaint;
    private Bitmap mBitmap;
    private Path mPath;
    private RectF mRectF;

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

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //1.画直线
        mStartX = 50;
        mStartY = 50;
        mEndX = 600;
        mEndY = 550;

        mPaint = new Paint();
        mPaint.setStrokeWidth(20);//画笔的宽度
        mPaint.setColor(Color.GREEN);//画笔颜色
        mPaint.setAntiAlias(true);//去锯齿

        //2.画圆
        mCenterX = 390;
        mCenterY = 390;
        mRadius = 200;

        ///3.空心圆
        mCirclePaint = new Paint();
        mCirclePaint.setStrokeWidth(20);//画笔的宽度
        mCirclePaint.setColor(Color.GREEN);//画笔颜色
        mCirclePaint.setAntiAlias(true);//去锯齿
        mCirclePaint.setStyle(Paint.Style.STROKE);//空心画笔

        //4.画图片
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);

        //5.三角形(多边形)
        int x1 = 50;
        int y1 = 50;
        int x2 = 700;
        int y2 = 80;
        int x3 = 300;
        int y3 = 650;

        //路径
        mPath = new Path();
        mPath.moveTo(x1,y1);
        mPath.lineTo(x2,y2);
        mPath.lineTo(x3,y3);
        mPath.lineTo(x1,y1);

        //扇形
        mRectF = new RectF(10, 10, 750, 750);
    }

    /**
     * Measure测量一个View的大小 (onMeasure)
     * measure() final 不能复写
     *
     * @param widthMeasureSpec 父容器对孩子的宽度的期望
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //MeasureSpec
        //未定义的模式,父容器对子view没有大小约束
        //MeasureSpec.UNSPECIFIED
        //父容器对子view的大小约束是精确的
        //MeasureSpec.EXACTLY
        //最大模式,
        //MeasureSpec.AT_MOST
        //获取测量模式
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        //大小
        int size = MeasureSpec.getSize(widthMeasureSpec);
        //mode:1,size:525(像素),420dpi  px = dp值 * 屏幕像素密度/160
        Log.d(TAG, "onMeasure: mode:"+(mode>>30)+",size:"+size);
    }

    /**
     * Layout摆放一个View的位置 (onLayout)
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r, b);
        Log.d(TAG, "layout: ");
    }

    /**
     * Draw画出View的显示内容 (onDraw)
     * @param canvas
     */
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        Log.d(TAG, "draw: ");
    }

    /**
     * 因为onDraw()有可能被调用好多次,所以尽量不要在里面new对象
     * @param canvas 画布
     *               Paint :画笔
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //6.裁剪,先裁切再画别的东西,比如图片
        //canvas.clipPath(mPath);
        //1.画线
        //canvas.drawLine(mStartX, mStartY, mEndX, mEndY,mPaint);
        //2.画圆
        //canvas.drawCircle(mCenterX,mCenterY,mRadius, mPaint);
        //3.空心圆
        //canvas.drawCircle(mCenterX,mCenterY,mRadius,mCirclePaint);
        //4.画图片
        //canvas.drawBitmap(mBitmap,0,0,mPaint);
        //5. 画三角形(多边形)
        //canvas.drawPath(mPath,mPaint);
        //7.画扇形
        //recf,矩形区域
        //startAngle,起始角度,0度水平向右,90度向下
        //sweepAngle 扇形扫过的角度
        //userCenter,true,
        canvas.drawArc(mRectF,0,120,false,mCirclePaint);
    }
}

2.圆环进度条

image.png
对于自定义view,很多时候需要使用到自定义属性,我们向实现一个view的自定义属性,需要遵循以下几部:
a.自定义一个CustomView(extends View )类
b.编写values/attrs.xml,在其中编写styleable和item等标签元素
c.在布局文件中CustomView使用自定义的属性(注意namespace)
导入自定义属性,以下两种方式都可(namespace)
http://schemas.android.com/apk/res/包名
http://schemas.android.com/apk/res-auto
d.在CustomView的构造方法中通过TypedArray获取

2.1 AttributeSet与TypedArray

构造方法中的有个参数叫做AttributeSet(eg: MyTextView(Context context, AttributeSet attrs) )这个参数看名字就知道包含的是参数的集合,那么我能不能通过它去获取我的自定义属性呢?
首先AttributeSet中的确保存的是该View声明的所有的属性,并且外面的确可以通过它去获取(自定义的)属性,怎么做呢? (就是便利出所有的自定属性)
其实看下AttributeSet的方法就明白了,下面看备注1的代码及打印结果。

    public CircleProgress(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
      for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attributeName = attrs.getAttributeName(i);
            String attributeValue = attrs.getAttributeValue(i);
            //因为它获取到的关联类型为资源id,所以不方便使用
            Log.d(TAG, "attributeName: "+attributeName+",attributeValueP:"+attributeValue);
        }

b.编写values/attrs.xml,在其中编写styleable和item等标签元素

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--declare-styleable name 声明标签内的所有属性,可以CircleProgress用-->
    <declare-styleable name="CircleProgress">
        <attr name="ring_color" format="color|reference"/>
        <attr name="ring_width" format="dimension"/>
        <attr name="circle_radius" format="dimension"/>
        <attr name="circle_color" format="color"/>
        <attr name="android:textSize"/>
        <attr name="android:textColor"/>
        <attr name="startAngle" format="float"/>
        <attr name="sweepAngle" format="float"/>
    </declare-styleable>
</resources>

c.在布局文件中CustomView使用自定义的属性(注意namespace)就是自己定义的继承View的类

public class CircleProgress extends View {
    private static final String TAG = "CircleProgress";
    private int mRingColor;
    private float mRingWidth;
    private int mCiclrColor;
    private float mCircleRadius;
    private float mTextSize;
    private int mTextColor;
    private float mStartAngle;
    private float mSweepAngle;
    private RectF mRectF;
    private Paint mRingPaint;
    private float mCenterX;
    private Paint mCirclePaint;
    private Paint mTextPaint;
    private float mDy;
    private String mText = "0 %";

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

    /**
     *
     * @param context
     * @param attrs 所有属性的集合
     */
    public CircleProgress(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
       /* for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attributeName = attrs.getAttributeName(i);
            String attributeValue = attrs.getAttributeValue(i);
            //因为它获取到的关联类型为资源id,所以不方便使用
            Log.d(TAG, "attributeName: "+attributeName+",attributeValueP:"+attributeValue);
        }*/

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
        if (ta != null){
            mRingColor = ta.getColor(R.styleable.CircleProgress_ring_color, 0);
            mRingWidth = ta.getDimension(R.styleable.CircleProgress_ring_width, 50);
            mCiclrColor = ta.getColor(R.styleable.CircleProgress_circle_color, 0);
            mCircleRadius = ta.getDimension(R.styleable.CircleProgress_circle_radius, 50);
            mTextSize = ta.getDimension(R.styleable.CircleProgress_android_textSize, 20);
            mTextColor = ta.getColor(R.styleable.CircleProgress_android_textColor, 0);
            mStartAngle = ta.getFloat(R.styleable.CircleProgress_startAngle, -90);
            mSweepAngle = ta.getFloat(R.styleable.CircleProgress_sweepAngle, 0);
        }

        mRingPaint = new Paint();
        mRingPaint.setAntiAlias(true);
        mRingPaint.setColor(mRingColor);
        mRingPaint.setStrokeWidth(mRingWidth);
        mRingPaint.setStyle(Paint.Style.STROKE);

        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(mCiclrColor);

        mTextPaint = new Paint();
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        //水平居中
        mTextPaint.setTextAlign(Paint.Align.CENTER);

        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        float fontHeight = fontMetrics.descent - fontMetrics.ascent;
        mDy = fontHeight/2-fontMetrics.descent;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        float left = 0.1f * width;
        float right = 0.9f * width;
        mRectF = new RectF(left, left, right, right);

        mCenterX = width/2;

       mCircleRadius =  mCircleRadius > width/4 ? width/4 : mCircleRadius;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //1.先画外环,扇形
        canvas.drawArc(mRectF,mStartAngle,mSweepAngle,false,mRingPaint);
        //2.画内圆
        canvas.drawCircle(mCenterX,mCenterX,mCircleRadius,mCirclePaint);
        //3.画文本
        canvas.drawText(mText,mCenterX,mCenterX+mDy,mTextPaint);
    }

    /**
     * 设置进度
     * @param progress 0-100  这个方法在activity中调用将进度的数据发送过来
     */
    public void setProgress(int progress) {
        mSweepAngle = progress * 360/100;
        mText = progress+ " %";
        //重新绘制
        //必须在主线程
        //invalidate();
        //非ui线程可用
        postInvalidate();
    }
}

d.模拟的进度数据 就是倒计时发送过去

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private CircleProgress cp;
    private Button bt;
    private int a=0;
    private Handler handler=new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if(msg.what==1){
                if(a<100){
                    a++;
                    handler.sendEmptyMessageDelayed(1,50);
                    cp.setProgress(a);
                }
            }
            return false;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        cp = (CircleProgress) findViewById(R.id.cp);
        bt = (Button) findViewById(R.id.bt);

        bt.setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt:
                handler.sendEmptyMessageDelayed(1,50);
                break;
        }
    }
}

上一篇下一篇

猜你喜欢

热点阅读