自定义View
一.xml的实质
- xml不是必须,布局可以代码写
- xml是为了开发者开发布局便利,谷歌给开发者开发糖
- 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
一.什么是自定义控件?
- 原生控件:SDK已经有,Google提供
- 自定义控件: 开发者自己开发的控件,分三种
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;
}
}
}