Android 自定义控件原理

来源:互联网 发布:淘宝招聘官网首页 编辑:程序博客网 时间:2024/05/01 11:06
# 自定义控件
* Android自身带的控件不能满足需求, 需要根据自己的需求定义控件.
## 自定义控件可以分为三大类型
### 1. 组合已有的控件实现
- 优酷菜单
1. 在xml布局里摆放好, OK
2. 给指定控件添加点击事件. OK
3. 根据业务逻辑,执行动画(旋转动画: 补间动画). OK
4. 菜单按钮的截获. OK
- 轮播图广告
1. 让图片滑动起来(ViewPager), OK
2. 让图片和文字,指示器对应起来, OK
3. 让轮播器无限循环
向右无限循环
0 -> 4 newPosition = position % 5
5 -> 0
6 -> 1
7 -> 2
8 -> 3
9 -> 4
10 -> 0
向左无限循环
设置到中间某个位置.
4. 轮播器自动轮询, OK
- 下拉选择框
* Button或ImageButton等自带按钮功能的控件会抢夺所在Layout的焦点.导致其他区域点击不生效.在所在layout声明一个属性

android:descendantFocusability="blocksDescendants"
* popupwindow获取焦点, 外部可点击
// 设置点击外部区域, 自动隐藏
popupWindow.setOutsideTouchable(true); // 外部可触摸
popupWindow.setBackgroundDrawable(new BitmapDrawable()); // 设置空的背景, 响应点击事件
popupWindow.setFocusable(true); //设置可获取焦点


### 2. 完全自定义控件.(继承View, ViewGroup)
* 1. 自定义开关
> - 1. 写个类继承View, OK
> - 1. 写个类继承ViewGroup, OK
> - 2. 拷贝包含包名的全路径到xml中, OK
> - 3. 界面中找到该控件, 设置初始信息, OK
> - 4. 根据需求绘制界面内容,OK
> - 5. 响应用户的触摸事件,OK
> - 6. 创建一个状态更新监听.OK
// 1. 声明接口对象
public interface OnSwitchStateUpdateListener{
// 状态回调, 把当前状态传出去
void onStateUpdate(boolean state);
}
public void setOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}
onSwitchStateUpdateListener.onStateUpdate(state);/
> - 7. 自定义属性
1. 在attrs.xml声明节点declare-styleable
<declare-styleable name="ToggleView">
<attr name="switch_background" format="reference" />
<attr name="slide_button" format="reference" />
<attr name="switch_state" format="boolean" />
</declare-styleable>

2. R会自动创建变量
attr 3个变量
styleable 一个int数组, 3个变量(保存位置)
3. 在xml配置声明的属性/ 注意添加命名空间
xmlns:itheima="http://schemas.android.com/apk/res/com.itheima74.toggleview"
itheima:switch_background="@drawable/switch_background"
itheima:slide_button="@drawable/slide_button"
itheima:switch_state="false" 4. 界面/外部, 收到事件.
4. 在构造函数中获取并使用
// 获取配置的自定义属性
String namespace = "http://schemas.android.com/apk/res/com.itheima74.toggleview";
int switchBackgroundResource = attrs.getAttributeResourceValue(namespace , "switch_background", -1);
> - Android 的界面绘制流程
测量 摆放 绘制
measure -> layout -> draw
onMeasure -> onLayout -> onDraw 重写这些方法, 实现自定义控件
都在onResume()之后执行
View流程 :onMeasure() (在这个方法里指定自己的宽高) -> onDraw() (绘制自己的内容)
ViewGroup流程
onMeasure() (指定自己的宽高, 所有子View的宽高)-> onLayout() (摆放所有子View) -> onDraw() (绘制内容)
* 2. 侧滑面板
### 3. 继承已有的控件实现(扩展已有的功能)

* 1. 包含下拉刷新功能的ListView
1. 添加了自定义的头布局
2. 默认让头布局隐藏setPadding.设置 -自身的高度
3. ListView下拉的时候, 修改paddingTop, 让头布局显示出来
4. 触摸动态修改头布局, 根据paddingTop.
- paddingTop = 0 完全显示
- paddingTop < 不完全显示 -64(自身高度)完全隐藏
- paddingTop > 0 顶部空白
5. 松手之后根据当前的paddingTop决定是否执行刷新
- paddingTop < 0 不完全显示, 恢复
- paddingTop >= 0 完全显示, 执行正在刷新...


#66 自定义开关
单独开一个类ToggleView

在布局中运用这个类
Android 的界面绘制流程
* 测量 摆放 绘制*
measure -> layout -> draw 一般子控件就不需要摆放 因为父容器会有所规定 但是如果控件是继承viewGroup的 话就要规定layout的布局 就像子控件是一个家具 不要定义放的位置 但是作为自定义一个房子就要只要子空间的摆放位置
* onMeasure -> onLayout -> onDraw 重写这些方法, 实现自定义控件*
onResume()之后执行
onMeasure() (在这个方法里指定自己的宽高) -> onDraw() (绘制自onMeasure() (指定自己的宽高, 所有子View的宽高)-> onLayout() (摆放所有子View) -> onDraw() (绘制内容)己的内容)


自定义属性的办法 在values下创建arrts.xml文件

1 <resources>
<declare-styleable name="ToggleView">
<attr name="switch_background" format="reference"/>
<attr name="slide_button" format="reference"/>
<attr name="switch_state" format="boolean"/>
</declare-styleable>
</resources>


2 在layout中配置变量
主要要在父布局中配置一个 每次可能不一样
xmlns:app="http://schemas.android.com/apk/res-auto"

3 在自定义的类中 找到各项的资源

publicToggleView(Context context,AttributeSet attrs) {
super(context,attrs);
init();
// 获取配置的自定义属性
String namespace = "http://schemas.android.com/apk/res-auto";
intswitchBackgroundResource = attrs.getAttributeResourceValue(namespace,"switch_background",-1);
intslideButtonResource = attrs.getAttributeResourceValue(namespace,"slide_button",-1);
mSwitchState= attrs.getAttributeBooleanValue(namespace,"switch_state", false);
setSwitchBackgroundResource(switchBackgroundResource);
setSlideButtonResource(slideButtonResource);


4类的实现
初始化一个画笔
private voidinit() {
paint=newPaint();
}


一个测量的方法 拿到背景 的大小
@Override
protected voidonMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
setMeasuredDimension(switchBackgroupBitmap.getWidth(),switchBackgroupBitmap.getHeight());
}


5通过画板将图案 绘制出来 在绘制的时候还要满足不同的需求
if(isTouchMode){
// 根据当前用户触摸到的位置画滑块

// 让滑块向左移动自身一半大小的位置
floatnewLeft =currentX-slideButtonBitmap.getWidth() /2.0f;

intmaxLeft =switchBackgroupBitmap.getWidth() -slideButtonBitmap.getWidth();

// 限定滑块范围
if(newLeft < 0){
newLeft =0;// 左边范围
}else if (newLeft > maxLeft) {
newLeft = maxLeft;// 右边范围
}

canvas.drawBitmap(slideButtonBitmap,newLeft,0,paint);protected void onDraw(Canvas canvas) {
// 1. 绘制背景
canvas.drawBitmap(switchBackgroupBitmap,0,0,paint);
}else{
// 根据开关状态boolean, 直接设置图片位置
if(mSwitchState){// 开
intnewLeft =switchBackgroupBitmap.getWidth() -slideButtonBitmap.getWidth();
canvas.drawBitmap(slideButtonBitmap,newLeft,0,paint);
}else{// 关
canvas.drawBitmap(slideButtonBitmap,0,0,paint);
}caseMotionEvent.ACTION_UP:
isTouchMode=false;
System.out.println("event: ACTION_UP: " + event.getX());
currentX= event.getX();

floatcenter =switchBackgroupBitmap.getWidth() /2.0f;

// 根据当前按下的位置, 和控件中心的位置进行比较.
booleanstate =currentX> center;

// 如果开关状态变化了, 通知界面. 里边开关状态更新了.
if(state != mSwitchState&&onSwitchStateUpdateListener!=null){
// 把最新的boolean, 状态传出去了
onSwitchStateUpdateListener.onStateUpdate(state);
}

mSwitchState= state;
break;
invalidate();//public interfaceOnSwitchStateUpdateListener{
// 状态回调, 把当前状态传出去
voidonStateUpdate(booleanstate);
}

public voidsetOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener= onSwitchStateUpdateListener;
}
自定义一个侧滑面板
定义一个类 继承一个ViewGroup
拿到第子控件

@Override
protected void onMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
View leftMenu = getChildAt(0);//拿到第一个控件
leftMenu.measure(leftMenu.getLayoutParams().width,heightMeasureSpec);//测量宽度和屏幕的高度
//测量控件宽高

View mainContent = getChildAt(1);//拿到主面板
mainContent.measure(widthMeasureSpec,heightMeasureSpec);
//侧量宽高
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
//放置子控件
@Override
protected voidonLayout(booleanchanged, intl, intt, intr, intb) {

//摆放的是左面板
View leftMenu = getChildAt(0);//拿到第一个也就是被隐藏的控件

leftMenu.layout(-leftMenu.getMeasuredWidth(),0,0,b);//将它放到一个负的它的宽度的位子
View main = getChildAt(1);//拿到主面板
main.layout(l,t,r,b);//将它放在这个viewGroup的位置
}

//设置一个触摸事件和判断的逻辑
@Override
public booleanonTouchEvent(MotionEvent event) {
switch(event.getAction()) {
caseMotionEvent.ACTION_DOWN: {
downX= (int) event.getX();//拿到开始滑动的坐标
break;
}
caseMotionEvent.ACTION_MOVE: {
moveX= (int) event.getX();//拿到移动的坐标比如拿到260
intsrollX = (downX-moveX);//-260
intnewScrollPosition = getScrollX() + srollX;//拿到0-260

if(newScrollPosition < -getChildAt(0).getMeasuredWidth()) {//划定左边距


scrollTo(-getChildAt(0).getMeasuredWidth(),0);

}else if(newScrollPosition >0) {//划定右边距
scrollTo(0,0);
}else{
scrollTo(srollX,0);
}
downX=moveX;
break;


}
caseMotionEvent.ACTION_UP:
// 根据当前滚动到的位置, 和左面板的一半进行比较
intleftCenter = (int) (-getChildAt(0).getMeasuredWidth() /2.0f);

if(getScrollX() < leftCenter) {
// 打开, 切换成菜单面板
currentState=MENU_STATE;//切换成面板状态
updateCurrentContent();
}else{
// 关闭, 切换成主面板
currentState=MAIN_STATE;
updateCurrentContent();
}

break;
default:
break;

}
return true;
}



//根据滑动的状态和滑动的长度来实现动画效果

private voidupdateCurrentContent() {
intstartX = getScrollX();
intdx =0;
if(currentState==MENU_STATE) {

dx = -getChildAt(0).getMeasuredWidth() - startX;
}else{
scrollTo(0,0);

dx = 0- startX;
}
intduration = Math.abs(dx * 2);
scroller.startScroll(startX,0,dx,0,duration);
invalidate();

}
//事件的拦截 如果是自己想要的事件就拦截下来 调用自己的方法 如果不是自己想要的时间就分发给其他的子控件来处理事件
public booleanonInterceptTouchEvent(MotionEvent ev) {
switch(ev.getAction()) {
caseMotionEvent.ACTION_DOWN:
downX= (int) ev.getX();
downY= ev.getY();
break;
caseMotionEvent.ACTION_MOVE:

floatxOffset = Math.abs(ev.getX() - downX);
floatyOffset = Math.abs(ev.getY() - downY);

if(xOffset > yOffset && xOffset >5) {// 水平方向超出一定距离时,才拦截
return true; // 拦截此次触摸事件, 界面的滚动
}

break;
caseMotionEvent.ACTION_UP:

break;

default:
break;
}
return super.onInterceptTouchEvent(ev);



0 0
原创粉丝点击