美文网首页
自定义View

自定义View

作者: 君袅 | 来源:发表于2019-03-17 18:44 被阅读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;
            }
        }
    }
    
    

    相关文章

      网友评论

          本文标题:自定义View

          本文链接:https://www.haomeiwen.com/subject/ghpumqtx.html