自定义控件的分类
[1]通过系统提供的原生控件进行组合 来达到自定义的需求
[2]定义一个类继承View(继承原生的类)
定义一个类继承ViewGroup (五大布局都继承自viewgroup)
下拉列表(原生控件进行组合)
功能分析:
由edittext 按钮 popupwindow listview 通过这样几个控件进行组合达到需求
实现步骤
**1先画布局定义布局 **
<RelativeLayout 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"
android:gravity="center_horizontal"
tools:context=".MainActivity" >
<EditText
android:id="@+id/et_number"
android:layout_width="250dp"
android:layout_height="wrap_content" />
<!--去掉背景-->
<!--android:background="@null"-->
<ImageButton
android:id="@+id/ib_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@id/et_number"
android:background="@null"
android:src="@drawable/down_arrow" />
</RelativeLayout>
2 当点击按钮的时候弹出popupwindow
//弹出popupwindow 宽高 和Edittext宽高一样
protected void showPopUpWindow() {
//[0]准备popupwindow 要展示的数据(listview)
ListView contentView = initListView();
//[1]构造popupwindow
Popupwindow没必要new多次, 因此先判断一下popupwindow是否为空
if (popupWindow == null) {
popupWindow = new PopupWindow(contentView, et_number.getWidth() - 8, 250, true);
popupwindow默认不让获取焦点(因此使用构造方法为4个参数的)
//参1:要展示的view 参2: 宽 参3: 高 参4:Boolean—popupwindow是否可以获取焦点
//设置背景
//点击popupwindow外面popupwindow不消失,想让popupwindow消失,因此给popupwindow设置背景
popupWindow.setBackgroundDrawable(new ColorDrawable());
}
//[2]展示popupwindow anchor:
//参1popupwindow依赖哪个控件(展示在哪个控件下方)
// 参2 偏移量x
//参3 偏移量y
//偏移量可以让尽量popupwindow与依赖的控件对齐的一致
popupWindow.showAsDropDown(et_number, 4, -4);
}
再设置adapter--点击事件
绘制实战--重写ondraw方法中—CANVAS.DRAW画
//将new paint实例放在构造方法中
//在ondraw方法中避免申请paint对象,因为有可能在画的过程中申请多次,最好写在构造方法中
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//[1]创建画笔类
mPaint = new Paint();
}
//[1]画线 两点确定一条线
canvas.drawLine(0,0,100,100,paint);
//参1 参2 起点坐标
//参3 参4 终点坐标
//参5 画笔
//[2]画圆 需要知道圆心和半径
//画圆 知道圆心 和 半径 通过cx cy确定圆心 radius:半径
canvas.drawCircle(100,100,30,mPaint);
//参1 参2 确定圆心坐标(在view上画圆,因此找圆心只要是view宽高的一半即可)
//参3 半径
//参4 paint
//修改画笔的(属性)
//修改画笔颜色(画笔默认的颜色是黑色的)
mPaint.setColor(Color.RED);
//设置画笔样式 空心(默认实心)
mPaint.setStyle(Style.STROKE);
//去除锯齿
mPaint.setAntiAlias(true);
// [3]画图片
//将图片转换成bitmap
// 2把haha.jpg转换成一个bitmap对象
mBitmap=BitmapFactory.decodeResource(
getResources(),R
.drawable.haha);
//画(bitmap)
canvas.drawBitmap(mBitmap,10,10,mPaint);
//参1 bitmap
//参2 距view 左边的位置
//参3 距view 顶部的位置
//参4 paint
//[4]画三角形(画路径)
//定义三个点 画一个三角形
int x1 = 100, y1 = 0;
int x2 = 20, y2 = 180;
int x3 = 180, y3 = 180;
//拿到path路径对象--先移动到第一个点--然后挨个连接--有头有尾(其实就是构造出三角形的路径)
mPath=new
Path();
mPath.moveTo(x1,y1); //先移动到x1 y1点
mPath.lineTo(x3,y3); //连接
mPath.lineTo(x2,y2);
mPath.lineTo(x1,y1);
//将构造的路径画出来(画三角形)
//把三个点连起来即为三角形
canvas.drawPath(mPath,mPaint);
//参1 path 参2 paint
// [5]画扇形 画扇形drawArc
//rectF控制画扇形的范围
canvas.drawArc(rectF,0,swapAngle,false,mPaint);
//参1 矩形区域(通过这个矩形来限定画的扇形的大小)
//参2 开始的一个角度,与水平向右的夹角(顺时针为正)
//参3 扫过的的一个角度,与水平向右的夹角(顺时针为正)
//参4 bollean(true 圆弧有边 false 圆弧没有边)
//参5 paint
//构造矩形
rectF=new
RectF(5,5,170,170);
//[6]动态画圆
//通过画扇形drawArc方法来画圆(调用一次方法只能画一段圆弧)
//我们只需模拟一些数据,动态的改变参3的值即可
//因此需要定义一个变量,接受传来的值
//将传来的值传给canvas.drawArc方法的参3(这个方法写在ondraw中)
//请求重新绘制
//设置点击事件开始—模拟数据画圆
// 点击按钮 动态画圆
public void click(View v) {
// 开启一个子线程
new Thread() {
public void run() {
// 模拟数据
for (int i = 1; i <= 100; i++) {
// 开始画
myView.startDraw(i);
// 睡眠50毫秒(因为画的太快,开不出效果,睡一会儿)(睡眠开子线程)
SystemClock.sleep(50);
}
}
;
}.start();
}
//找到所要画圆的view,创建方法,传递数据, 并请求重新绘制
public void startDraw(int x) {
//定义一个变量(成员变量)进行传值
mprogress = x;
//请求重新绘制
// 请求重新绘制 --->onDraw方法就会执行
// invalidate();
// 如果不是ui 线程 应该调用下面这个方法()--请求重新绘制 --->onDraw方法就会执行
postInvalidate();
}
//重写ondraw方法,画圆弧,
//往当前控件上画内容
@Override
protected void onDraw(Canvas canvas) {
// rectF控制画扇形的范围
数据传入参数3
float swapAngle = mprogress / 100f * 360;
canvas.drawArc(rectF, 0, swapAngle, false, mPaint);
// 参1 矩形区域(通过这个矩形来限定画的扇形的大小)
// 参2 开始的一个角度,与水平向右的夹角(顺时针为正)
// 参3 扫过的的一个角度,与水平向右的夹角(顺时针为正)
// 参4 bollean(true 圆弧有边 false 圆弧没有边)
// 参5 paint
}
//构造矩形
rectF=new RectF(5,5,170,170);
可滑动开关 –继承view(自定义控件)
需求分析 :实际是由2张图片 叠加到一起组成一个View
自己做自定义控件就两种方式,继承view还是继承viewgroup---如果需要包裹孩子就继承viewgroup
定义一个类继承view---添加两个参数的构造方法
public class ToogleView extends View {
public ToogleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
使用该view--在xml中声明
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:itheima="http://schemas.android.com/apk/res/com.itheima.toogleview"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.itheima.toogleview.ToogleView
android:id="@+id/toogleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
itheima:toogleState="false" />
</RelativeLayout>
测量-根据自己的需求对当前控件进行测量—调用onmeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自己测量控件的宽和高 和当前背景图片一样宽 一样高
setMeasuredDimension(toogleBgBitmap.getWidth(), toogleBgBitmap.getHeight());
}
///把图片变成bitmap---为了获取背景图片的宽高
// [1]找到背景图片和滑动块图片 变成bitmap
toogleBgBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.toogle_background);
toogleSlideBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.toogle_slidebg);
不用排版,父类已经排好版
控件上绘制内容—重写ondraw—拿掉super—画好之后控件就展示上来了
// 往当前控件上画内容 要重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
//画开关背景--画图片的时候不用paint
canvas.drawBitmap(toogleBgBitmap, 0, 0, null);
//画滑动块--画图片的时候不用paint
canvas.drawBitmap(toogleSlideBitmap, slideLeftPosition, 0, null);
}
让当前的view处理事件—画好开关后——需要重写onTouchEvent—有按下 移动 抬起 等事件—返回true(当前view消费事件)
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN://按下
break;
case MotionEvent.ACTION_MOVE://移动
break;
case MotionEvent.ACTION_UP: // 手指抬起
break;
}
return true; // 代表让当前控件处理事件 消费事件
}
按钮移动效果
获取按下坐标—获取移动后的坐标—算出移动距离—让滑块移动这么长的距离(移动着么长距离其实就是重新再新的坐标上绘制按钮)
//1获取手指按下的x坐标
downX=event.getX();
//1获取手指移动后坐标
float moveX = event.getX();
//2 算出移动距离
float distanceX = moveX - downX;
//指定滑块移动到的位置---赋值给绘制滑块的参数3()
slideLeftPosition+=distanceX;
//更改起始点
// [4]改变一下起始点
downX=moveX;
//请求重新绘制 onDraw方法就会执行
invalidate();
限定滑块的左右边界
//右边界最大值—背景宽-滑块的宽
slideLeftMax = toogleBgBitmap.getWidth() - toogleSlideBitmap.getWidth();
//对边界进行判断
if (slideLeftPosition <= 0) {
slideLeftPosition = 0;
} else if (slideLeftPosition >= slideLeftMax) {
slideLeftPosition = slideLeftMax;
}
当手指抬起时,滑倒一个位置,抬起后接着滑动到左边或者右边
//当手指抬起时,获取到手指在的 位置,判断滑块的中心店位置,过去
//1算出背景中心点位置
float tooglebgCenterPosition = toogleBgBitmap.getWidth() / 2;
//2 算出滑动块的中心点位置 = slideLeftPosition + 滑块背景的一半
float sliecenterPosition = slideLeftPosition
+ toogleSlideBitmap.getWidth() / 2;
//判断滑块与背景的相对位置
if (sliecenterPosition <= tooglebgCenterPosition) {
// 开关处于关
slideLeftPosition = 0;
} else {
slideLeftPosition = slideLeftMax;
}
View状态发生改变时的回调事件--实现开关事件对应的回调方法*
//我们想要在开关开启或者关闭的时候,执行一些逻辑要暴露此方法
//回调其实就是多态的一个应用定义接口—暴回调方法
// 定义接口
public interface OnToogleViewListener {
// 当开关的状态发生改变的时候调用
void onToogleState(boolean state);
}
//开关状态发生变化时调用该方法
//当手指抬起且开关状态发生变化—调用该方法判断手指是否抬起器开关状态是否改变定义变量判断是否抬手
/**
* 是否抬起手 默认false
*/
private boolean isHandup;
//当手指抬起的时候置为true
isHandup = true;
//当我们判断为抬起之后将其设置为false
isHandup = false;
//定义开关默认状态
/**
* 代表开关的默认状态
*/
private boolean isOpen;
//当手指抬起获取滑动后的开关状态
//3回调我们定义接口方法
if (isHandup){
isHandup = false;
//4获取滑动后的一个状态
boolean isOpenTemp = slideLeftPosition>0;
//判断如果临时状态与默认状态不一致则说明开关状态发生变化了
if (isOpen!=isOpenTemp && mListener!=null) {
//说明开关的状态改变了 我们就要触发我们定义的回调方法
mListener.onToogleState(isOpenTemp);
//发生改变后将发生改变后的状态—赋值给默认状态
isOpen = isOpenTemp;
}
}
//当开关状态放生变化时,触发回调方法(返回相应的值)
mListener.onToogleState(isOpenTemp);
//发生改变后将发生改变后的状态—赋值给默认状态
isOpen = isOpenTemp;
//给view设置监听—传入接口类型(子类)—给接口类型成员变量赋值
/**
* 设置开关的监听器
*/
public void setOnToogleViewListener(OnToogleViewListener l) {
this.mListener = l;
}
//使用view的监听事件-- ,当监听到改变后执行触发后的方法 toogleView.setOnToogleViewListener(new OnToogleViewListener() {
//这个什么时候被触发呢?
@Override
public void onToogleState(boolean state) {
if (state) {
Toast.makeText(getApplicationContext(), "开", 1).show();
}else {
Toast.makeText(getApplicationContext(), "关", 1).show();
}
}
});
设置点击事件—也包含在状态改变中
当按下时,获取按下的时间
long startTime = System.currentTimeMillis();
当手指抬起时记住抬起的时间
long endTime = System.currentTimeMillis();
判断
时间间隔小于200ms为点击事件
手指抬起的位置大于滑块的右边界
Else手指抬起的位置大于滑块的左边界
Else(大于200ms为滑动事件)
case MotionEvent.ACTION_UP: // 手指抬起
int endTime = (int) (System.currentTimeMillis() - startTime);
System.out.println("endTiem:" + endTime);
if (endTime < 200 && event.getX() > toogleSlideBitmap.getWidth()) {
// 说明是点击事件
slideLeftPosition = slideLeftMax;
} else if (endTime < 200) {
slideLeftPosition = 0;
} else {
// 1算出背景中心点位置
float tooglebgCenterPosition = toogleBgBitmap.getWidth() / 2;
// 2 算出滑动块的中心点位置 = slideLeftPosition + 滑块背景的一半
float sliecenterPosition = slideLeftPosition
+ toogleSlideBitmap.getWidth() / 2;
if (sliecenterPosition <= tooglebgCenterPosition) {
// 开关处于关
slideLeftPosition = 0;
} else {
slideLeftPosition = slideLeftMax;
}
}
isHandup = true;
break;
}
给view设置自定义属性
在res/values下创建attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="toogleView">
<attr name="toogleState" format="boolean" />
</declare-styleable>
</resources>
在layout布局中声明
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:kailing="http://schemas.android.com/apk/res/com.kailing.toogleview"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.kailing.toogleview.ToogleView
android:id="@+id/toogleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
kailing:toogleState="false" />
</RelativeLayout>
在构造方法中获取属性值
// [2]通過AttributeSet获取属性值
String namespace = "http://schemas.android.com/apk/res/com.kailing.toogleview";
//参1 根据命名空间取值,参2属性名,参3 缺省值(默认值)
boolean toogleState = attrs.getAttributeBooleanValue(namespace, "toogleState", false);
//使用—属性
// [3]调用setToogleState
setToogleState(toogleState);
//[7]view自定义属性
//1)在values下创建一个attrs.xml 文件
//2)xml里面的内容抄袭系统定义好的
//3)在布局中使用 自己定义一个命名空间
//4)在当前的view的构造方法里同AttributeSet获取我们定义的值
ViewGroup绘制流程
定义一个类继承viewgroup(会自动重写onLayout方法—就在这个方法里完成排版)
//定义一个类继承viewgroup(会自动重写onLayout方法—就在这个方法里完成排版)
public class MyViewGroup extends ViewGroup {
//添加两个参数的构造方法
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
布局文件中使用(在这里自view还是看不见的)
<RelativeLayout 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"
tools:context=".MainActivity" >
<com.kailing.viewgroup.MyViewGroup
android:id="@+id/myViewGroup1"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
</com.kailing.viewgroup.MyViewGroup>
</RelativeLayout>
自己定义的viewgroup没有对孩子进行测量,和排版所以group里面的孩子显示不出来
测量—onmeasure—测量孩子
当前重写onmeasure是对当前自己的groupview进行测量,让父类进行测量即可
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1获取viewGroup的孩子
// 参1因为就一个孩子--写0即可,多个孩子for循环即可
// for (int i = 0; i < getChildCount(); i++) {}
View childAt = getChildAt(0);
//2//对孩子进行测量—实际调用孩子的measure方法
//(参数写0是个非法值,当系统发现写0会采用默认的测量对当前的孩子进行测量)(默认的测量:孩子在声明的时候写的值--对应的模式)
childAt.measure(0, 0);// 这行代码执行完--测量完了
//3 获取孩子的宽度 高度(获取测量之后 的宽度和高度)
measuredWidth = childAt.getMeasuredWidth();
measuredHeight = childAt.getMeasuredHeight();
// getMeasuredWidth 获取到测量之后的值
// getWidth()当view排版后才可以获取
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
排版—完成对孩子进行排版
// 要在这个方法里面完成对孩子进行排版
// 参数
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//找到孩子
View childAt = getChildAt(0);
//对孩子进行排版
childAt.layout(l, t, measuredWidth, measuredHeight);
}
绘制—不需要重写ondraw方法