自定义View

作者: 夜沐下的星雨 | 来源:发表于2020-06-06 10:25 被阅读0次

一、xml的实质

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

二.View和ViewGroup

View

View是用户界面一个组件(控件)
View是一个矩形
View的职责是绘制和事件处理

ViewGroup

ViewGroup是一个特殊的View,它继承自View
ViewGroup可包含其他View(孩子)
ViewGroup常用layout的基类
ViewGroup定义了孩子的布局参数(带layout_前缀的属性)

View和ViewGroup的关系
继承关系:

view 继承.PNG
组合关系:
view 组合.PNG

三.什么是自定义控件?

原生控件:SDK已经有,Google提供
自定义控件: 开发者自己开发的控件,分三种
a. 组合式控件:将现有控件进行组合,实现功能更加强大控件;
b. 继承现有控件: 对其控件的功能进行拓展;
c. 重写View(ViewGroup)实现全新的控件.

四.为什么要自定义View?

原有控件无法满足我们的需求,所以需要自己实现想要的效果。

五.组合式控件下拉选择框

模块化思想,提高代码复用率

功能分析:
a. 点击箭头,弹出下拉列表 (Popupwindow + ListView)
b. 点击列表选项,在编辑框中显示内容

实现步骤:
a.继承布局,重写构造;
b.创建布局xml,加入到自定义控件里面;

实现对应的功能.
xml布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <EditText
        android:id="@+id/et"
        android:layout_margin="10dp"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

    <ImageView
        android:id="@+id/iv"
        android:layout_alignParentRight="true"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:src="@drawable/down_arrow"
        android:layout_width="50dp"
        android:layout_height="50dp" />
</RelativeLayout>

创建SpinnerView 类继承RelativeLayout

public class SpinnerView extends RelativeLayout {

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

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

    //必须有的构造
    public SpinnerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        //需要将写的布局添加到这个自定义view中
        LayoutInflater.from(getContext()).inflate(R.layout.view_spinner, this);
        initView();
        initData();
    }

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

    private void initView() {
        mEt = findViewById(R.id.et);
        mIv = findViewById(R.id.iv);

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

    private void showPop() {
        //判断是否为空,实现PopupWindow复用
        if (mPop == null){
            mPop = new PopupWindow(mEt.getWidth(),400);
            ListView listView = new ListView(getContext());
            //listView.setBackgroundResource(R.drawable.listview_background);
            listView.setAdapter(new ArrayAdapter<String>(getContext(),android.R.layout.simple_list_item_1,mData));
            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());
                    mPop.dismiss();
                }
            });
            //PopupWindow三要素之View
            mPop.setContentView(listView);
            mPop.setBackgroundDrawable(new ColorDrawable());
            mPop.setOutsideTouchable(true);
            mPop.setFocusable(true);
        }
        mPop.showAsDropDown(mEt);
    }
}

六、继承现有控件

在继承时View主要重写的构造方法分别代表的含义:

     /**
     * 代码操作的时候,直接new对象
     * @param context
     */
    public CView(Context context) {
        super(context);
    }

    /**
     * 布局文件中设置属性
     * @param context
     * @param attrs
     */
    public CView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 自定义属性
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public CView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

实现带有删除线的TextView

public class DeleteTextview extends TextView {
    public DeleteTextview(Context context) {
        super(context);
    }

    public DeleteTextview(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

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

    public void setDeleteLine(){
        getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG|Paint.ANTI_ALIAS_FLAG);
    }
}

实现并找到DeleteTextview 控件 调用setDeleteLine() 即可

七、View的绘制流程

a、Measure测量一个View的大小 (onMeasure)
b、Layout摆放一个View的位置 (onLayout)
c、Draw画出View的显示内容 (onDraw)

其中measure是final的,无法重写,虽然layout,draw不是final的,但是也不建议重写该方法。
这三个方法都已经写好了View的逻辑,如果我们想实现自身的逻辑,而又不破坏View的工作流程,可以重写onMeasure、onLayout、onDraw方法。

onMeasure:

onMeasure(int widthMeasureSpec, int heightMeasureSpec)是View让其父节点知道它想要多大的尺寸。方法的参数widthMeasureSpec和heightMeasureSpec代表 测量模式和宽高的大小。它本身是一个32位的数据,前两位代表模式,后30位代表尺寸大小。

onLayout ():

onLayout 让Android知道View在其父控件中的位置,即距父控件四边的距离left、right、top、bottom。布局是绘图的基础,只有完成了布局,才能对View进行绘图。

onDraw():

onDraw()绘图的前提是已经对View进行了量算和布局,View通过调用draw()方法进行绘图,绘图的目的就是让View在UI界面上呈现出来。

如何获取。参考附件代码:

   int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   int widthSize = MeasureSpec.getSize(widthMeasureSpec);

在对View进行测量时,Android提供了三种测量模式:
onMeasure包含了3种测量模式:UNSPECIFIED,EXACTLY,AT_MOST。

  1. EXACTLY ( 精确值模式):父view已经强制设置了子view的大小,一般是MATCH_PARENT和固定值
  2. UNSPECIFIED (不指定其大小测量的模式):父view对子view没有任何限制,子view可以是任何大小
  3. AT_MOST(最大值模式):子view限制在一个最大范围内,一般是WARP_CONTENT-包裹内容

绘制实战

  1. 画直线:
int startX = 5;
int startY = 100;
int stopX = 195;
int stopY = 100;
canvas.drawLine(startX, startY, stopX, stopY, mPaint);
  1. 画圆
canvas.drawCircle(100, 100, 40, mPaint);

3.画空心圆

 //还是画圆,改动画笔即可
mPaint.setAntiAlias(true);//去锯齿
mPaint.setStyle(Style.STROKE);//改变style可以画出空心圆
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.GREEN);
  1. 画图片
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
  1. 画三角形(多边形)
mPath = new Path();
//规划三角形的路径
int x1 = 100, y1 = 5;
int x2 = 195, y2 = 195;
int x3 = 5, y3 = 195;

mPath.moveTo(x1, y1);
//连接第一个点和第二个点
mPath.lineTo(x2, y2);
mPath.lineTo(x3, y3);
mPath.lineTo(x1, y1);
canvas.drawPath(mPath, mPaint);

6.裁剪

canvas.clipPath(mPath);//先裁切再画别的东西,比如图片
  1. 画扇形(放在拆剪后边画不出来)
mOval = new RectF(5, 5, 195, 195);
int startAngle = -90;
int sweepAngle = 60;
boolean useCenter = false;//是否画扇形的两条边
canvas.drawArc(mOval, startAngle, sweepAngle, useCenter, mPaint);

案例

1.小圆球
实现的功能:手指在屏幕上滑动,红色的小球始终跟随手指移动。

实现的思路:
1)自定义View,在onDraw中画圆作为小球;
2)重写自定义View的onTouchEvent方法,记录触屏坐标,用新的坐标重新绘制小球;
3)在布局中引用自定义View布局,运行程序,实现跟随手指移动效果。

关键技术点:自定义View应用、触摸事件处理、canvas绘图、Paint应用。

public class BallView extends View {
    private final Bitmap mBitmap;
    private final Paint mPaint;
    private float mMoveX = 60;
    private float mMoveY = 60;

    public BallView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ball);
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //1.画小球(图片) 使用触摸时的位置作为左上角
        //canvas.drawBitmap(mBitmap, mMoveX, mMoveY, mPaint);
        //2.画圆,使用了触摸屏幕时的位置作为圆心
        canvas.drawCircle(mMoveX,mMoveY,60,mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取触摸屏幕时触摸点的位置
        mMoveX = event.getX();
        mMoveY = event.getY();
        //重新绘制界面
        invalidate();
        return true;
    }
}

2.圆环进度条

  • 对于自定义view,很多时候需要使用到自定义属性,我们向实现一个view的自定义属性,需要遵循以下几部:

a.自定义一个CustomView(extends View )类
b.编写values/attrs.xml,在其中编写styleable和item等标签元素
c.在布局文件中CustomView使用自定义的属性(注意namespace)
导入自定义属性,以下两种方式都可(namespace)

d.在CustomView的构造方法中通过TypedArray获取

2.1 AttributeSet与TypedArray

构造方法中的有个参数叫做AttributeSet(eg: MyTextView(Context context, AttributeSet attrs) )这个参数看名字就知道包含的是参数的集合,那么我能不能通过它去获取我的自定义属性呢?

首先AttributeSet中的确保存的是该View声明的所有的属性,并且外面的确可以通过它去获取(自定义的)属性,怎么做呢?

其实看下AttributeSet的方法就明白了,下面看备注1的代码及打印结果。

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

        int count = attrs.getAttributeCount();
        for (int i = 0; i < count; i++) {
            String attrName = attrs.getAttributeName(i);
            String attrVal = attrs.getAttributeValue(i);
            Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);
        }

        // ==>use typedarray ...

    }

通过AttributeSet获取的值,如果是引用都变成了@+数字的字符串。你说,这玩意你能看懂么?那么你看看最后一行使用TypedArray获取的值,是不是瞬间明白了什么。

TypedArray其实是用来简化我们的工作的,比如上例,如果布局中的属性的值是引用类型(比如:@dimen/dp100),如果使用AttributeSet去获得最终的像素值,那么需要第一步拿到id,第二步再去解析id。而TypedArray正是帮我们简化了这个过程。

贴一下:如果通过AttributeSet获取最终的像素值的过程:

int widthDimensionId = attrs.getAttributeResourceValue(0, -1);
Log.e(TAG, "layout_width= "+getResources().getDimension(widthDimensionId));

ok,现在别人问你TypedArray存在的意义,你就可以告诉他了。

代码: values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyProgressBar">
        <!--使用系统的宽高属性-->
        <attr name="android:layout_width"/>
        <attr name="android:layout_height"/>
        <!--自定义的-->
        <!--外环颜色-->
        <attr name="ringColor" format="color"/>
        <!--外环宽度-->
        <attr name="ringWidth" format="dimension"/>
        <!--内圆颜色-->
        <attr name="circleColor" format="color"/>
        <!--进度文本大小颜色-->
        <attr name="android:textSize"/>
        <attr name="android:textColor"/>
        <!--进度 扇形的扫过角度-->
        <attr name="sweepAngle" format="integer"/>
        <attr name="startAngle" format="integer"/>
    </declare-styleable>
</resources>

自定义View:

public class MyProgressBar extends View {
    private static final String TAG = "MyProgressBar";
    private float mHeight;
    private float mWidth;
    private int mRingColor;
    private float mRingWidth;
    private int mCircleColor;
    private float mTextSize;
    private int mTextColor;
    private int mStartAngle;
    private int mSweepAngle;
    private float mRadius;
    private float mCenterXY;
    private Paint mCirclePaint;
    private Paint mTextPaint;
    private String mProgress;
    private RectF mRectF;
    private Paint mSweepPaint;
    private final float mDy;

    public MyProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        /*for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attributeName = attrs.getAttributeName(i);
            //如果是引用类型的话,attributeValue就变成了@+数字的字符串
            String attributeValue = attrs.getAttributeValue(i);
            //getResources().getColor(attributeValue)
            //getResources().getString(attributeValue)
            Log.d(TAG, "MyProgressBar: "+"attributeName:"+attributeName+",attributeValue"+attributeValue);
        }*/
        //系统提供的一个帮助类,可以直接过去里面的属性值,就算是引用类型的,也可以直接获取
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyProgressBar);
        if (ta != null){
            //获取属性
            mHeight = ta.getDimension(R.styleable.MyProgressBar_android_layout_height, 200);
            mWidth = ta.getDimension(R.styleable.MyProgressBar_android_layout_width, 200);
            mRingColor = ta.getColor(R.styleable.MyProgressBar_ringColor, 0);
            mRingWidth = ta.getDimension(R.styleable.MyProgressBar_ringWidth, 10);
            mCircleColor = ta.getColor(R.styleable.MyProgressBar_circleColor, 0);
            mTextSize = ta.getDimension(R.styleable.MyProgressBar_android_textSize, 16);
            mTextColor = ta.getColor(R.styleable.MyProgressBar_android_textColor, 0);
            mStartAngle = ta.getInteger(R.styleable.MyProgressBar_startAngle, -90);
            mSweepAngle = ta.getInteger(R.styleable.MyProgressBar_sweepAngle, 0);

            ta.recycle();//释放资源
        }

        //获取圆的画笔
        mCirclePaint = new Paint();
        mCirclePaint.setColor(mCircleColor);
        mCirclePaint.setAntiAlias(true);//设置画笔抗锯齿
        //获取文本画笔
        mTextPaint = new Paint();
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);//设置文本对齐方式

        //获取绘制文本对象
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        float v1 = fontMetrics.descent - fontMetrics.ascent;
        mDy = v1/2-fontMetrics.descent;

        float v = mSweepAngle * 1.0f / 360 * 100;
        mProgress = (int)v+" %";
        //获取圆环画笔
        mSweepPaint = new Paint();
        mSweepPaint.setAntiAlias(true);
        mSweepPaint.setColor(mRingColor);
        mSweepPaint.setStrokeWidth(mRingWidth);
        mSweepPaint.setStyle(Paint.Style.STROKE);//绘制圆环
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int max = (int) Math.max(mWidth, mHeight);
        setMeasuredDimension(max,max);
        //setMeasuredDimension((int) mWidth,(int)mHeight);
        mRadius = max*1.0f/4;
        mCenterXY = max *1.0f/2;

        float v = max * 0.9f;
        mRectF = new RectF(max*0.1f, max*0.1f,v , v);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //1.环里面的圆
        canvas.drawCircle(mCenterXY,mCenterXY,mRadius,mCirclePaint);
        //2.画文字
        canvas.drawText(mProgress,mCenterXY,mCenterXY+mDy,mTextPaint);

        //3.画进度,扇形
    canvas.drawArc(mRectF,mStartAngle,mSweepAngle,false,mSweepPaint);
    }

    /**
     *
     * @param progress 0-100
     */
    public void setProgress(int progress) {
        mSweepAngle =(int)(progress*3.6f) ;
        mProgress = progress+" %";
        invalidate();//只能在ui线程中调用
        //postInvalidate();// 可以在非ui线程中调用
    }
}

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:xts="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"
    tools:context="com.edu.cumulus.balldemo.MainActivity">

    <com.edu.cumulus.balldemo.MyProgressBar
        android:id="@+id/myProgressBar"
        android:background="#55000000"
        xts:circleColor="@color/colorAccent"
        xts:ringColor="@color/colorPrimary"
        xts:ringWidth="20dp"
        xts:sweepAngle="89"
        xts:startAngle="-90"
        android:text="@string/app_name"
        android:textSize="20sp"
        android:textColor="#ffffff"
        android:layout_width="300dp"
        android:layout_height="300dp" />

    <Button
        android:id="@+id/btn"
        android:text="开始"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

MainActivity使用,点击按钮,模拟下载:

  private void start() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    mMyProgressBar.setProgress(i);
                }
            }
        }).start();
    }

相关文章

网友评论

    本文标题:自定义View

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