今天要给大家带来一个自定义控件,这个控件在平板qq HD中有放上效果图
我就截图了我的设备上的一张图,是弹出的状态.如果收起来的时候,覆盖的半透明的白色就会消失,并且弹出来的小菜单都会收回.这就是这个控件的一个简单的介绍,而今天就要带大家来完成这个控件.
先放上实现好的效果图:
从效果上可以看出我们需要实现以下功能:
1.菜单收起来的时候就是一个很普通的图片
2.菜单弹出的时候需要给所在的容器覆盖一层白色半透明的层
3.弹出和收回的过程中,主菜单有一个旋转的动画,其余的小菜单有一个平移动画
4.点击小菜单的时候能调用一个自定义的回调接口,通知用户当前点击是第几个孩子,供用户编写之后的逻辑性代码
以上就是这个控件需要有的功能.下面小金子带大家来实现这个控件.首先我们先分析一下:
1.要实现弹出的时候有一层覆盖的效果,其实很容易可以想到其实你所看到的整个屏幕的大小这个控件的大小,这样子点击主菜单的时候才可以达到覆盖的效果.
原理:
a)其实这个控件是填充父容器的,收缩的时候背景颜色是完全透明的.所以你只能看到右下角的那个主菜单
b)展开的时候无非就是换了一层白色的半透明的颜色,所以看上去是覆盖的效果,下图红色框框包围的就是控件的大小
2.主菜单和小菜单的位置问题
原理:
a)从图中可以看出,我们的自定义控件是有孩子的,所以这就需要我们继承ViewGroup来实现
b)既然有孩子,那控件就承担着安排孩子位置的任务,所以你展开的时候看到的主菜单和小菜单的呈线性排列其实是这个自定义控件安排的
3.展开和收缩的效果实现
原理:
a)收缩的时候,就只让主菜单显示,其余小菜单都隐藏,自身背景变成完全透明
b)展开的时候主菜单和小菜单们都显示,并且让自身背景变成白色半透明的
c)在展开和收缩的时候加上响应的动画即可
4.使用方面的问题
这个控件会把第一个孩子当成主菜单,剩余的孩子当成可以弹出的小菜单
分析完毕之后,下面来写我们的代码:
一.首先起一个名字,并且重写几个构造方法:
这里的写法几乎是每一个自定义控件都一样的写法了,
首先继承ViewGroup或者View
重写构造函数,然后进行一些初始化的工作,这里的初始化工作就是initData方法,里面代码的意思就是让自己的背景颜色变成关闭时候的颜色,这里用了一个变量
/**
* 菜单开启的时候的背景颜色
*/
private int openColor = Color.parseColor("#99ffffff");
/**
* 菜单关闭的时候的背景颜色
*/
private int closeColor = Color.parseColor("#00ffffff");
也就是上述分析的时候 半透明和 全透明的颜色值
二.本来需要重写onMeasure方法,但是我们这个控件就是需要填充父容器的,所以这里就不在重写了,如果你们对这个方法不了解,可以参看我另一篇博客:
http://blog.csdn.net/u011692041/article/details/50598565
三.重写onLayout方法,这个方法从方法名字上就能看出,其实这个方法就是对孩子的位置进行设置,说白了就是安排你这个控件的所有孩子应该站在哪个区域.就是实现了我们之前看到的主菜单和小菜单在右下角从上到下排列
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 计算所需的参数
compute();
// 循环集合中的各个菜单的位置信息,并让孩子到这个位置上
for (int i = 0; i < rects.size(); i++) {
// 循环中的位置
RectEntity e = rects.get(i);
// 循环中的孩子
View v = getChildAt(i);
// 让孩子到指定的位置
v.layout(e.leftX, e.leftY, e.rightX, e.rightY);
// 如果不是第一个孩子,就默认让它隐藏
if (i > 0) {
v.setVisibility(View.INVISIBLE);
}
}
}
这段代码的意思就是,循环所有的孩子,调用孩子的layout(int,int,int,int)方法安排孩子的位置.因为初始化的时候不是弹出状态,所以不是主菜单就全部隐藏,这也就是下面这段代码的意思
而我为了代码的清晰,计算所有孩子的位置信息我写了一个方法,就是代码中第一句所调用的方法,目的就是计算出所有孩子位置的信息并且放到集合中
/**
* 计算需要的数据
*/
private void compute() {
// 清空集合
rects.clear();
//如果一个孩子都没有,那就是用户使用错误,直接返回
if (getChildCount() == 0) {
return;
}
this.rightMargin = getPaddingRight();
this.bottomMargin = getPaddingBottom();
// 拿出第一个孩子作为主菜单
menu = getChildAt(0);
// 主菜单的点击事件监听
menu.setOnClickListener(this);
//获取当前空间的宽和高
myWidth = getWidth();
myHeight = getHeight();
//创建一个矩形对象
RectEntity entity = new RectEntity();
//计算矩形的左上角的点的坐标和右下角的点的坐标
entity.rightX = myWidth - rightMargin;
entity.rightY = myHeight - bottomMargin;
entity.leftX = entity.rightX - menu.getMeasuredWidth();
entity.leftY = entity.rightY - menu.getMeasuredHeight();
//添加第一个矩形对象到集合中
rects.add(entity);
//获取所有孩子的个数
int childCount = getChildCount();
//第一个孩子已经安排为主菜单,所以这里从第二个开始循环
for (int i = 1; i < childCount; i++) {
//拿到一个孩子创建一个相应的矩形对象
View view = getChildAt(i);
//设置点击事件
view.setOnClickListener(this);
entity = new RectEntity();
//计算这个孩子在父容器中的位置,也就是矩形确定的区域
entity.leftY = rects.get(0).leftY - i * (view.getMeasuredHeight() + betweenMargin);
entity.rightY = entity.leftY + view.getMeasuredHeight();
entity.rightX = rects.get(0).rightX;
entity.leftX = entity.rightX - view.getMeasuredWidth();
//同样添加到集合中
rects.add(entity);
}
}
这段代码很重要,因为孩子显示在哪里完全就是这个方法所计算的.这里做一个解释:
可以看到我们的主菜单显示在最右下角,那么它的位置是如何计算出来的呢?
同理我们算出rightY的值
然后根据控件自身的宽和高
在rightX和rightY的基础上减去自身的宽和高得到leftX和leftY两个参数
这样子一个控件的位置也就被定下了,也就相当于矩形的左上角和右下角坐标定了就能确定矩形的区域是一样的道理,其他的小菜单的参数就可以参照这个主菜单的参数啦
而RectEntity是我封装一个类,因为使用一个对象显然比使用四个变量要简单,也更面向于对象编程
/**
* 一个实体类,描述一个矩形的左上角的点坐标和右下角的点的坐标
*
* @author cxj QQ:347837667
* @date 2015年12月22日
*
*/
public class RectEntity {
// 左上角横坐标
public int leftX;
// 左上角纵坐标
public int leftY;
// 右下角横坐标
public int rightX;
// 右下角纵坐标
public int rightY;
}
所以计算的这个方法也就基本上看懂了吧,里面还有注册点击事件,可以看到实现了点击的监听接口是控件本身,这是为了后面点击主菜单有效果而注册的,下面会详细说到
到这里,控件的位置已经安置完毕,当前的效果是如下图所示:
除了主菜单,其他的小菜单都是隐藏状态
四.好,那我们接下去写点击之后的动画效果和覆盖的效果
实现点击主菜单会弹出小菜单,这个就必须去监听主菜单的点击事件,也就是我们第一个孩子的点击事件,而注册点击事件的步骤已经在计算的方法中注册了
因为其他地方要用到主菜单所以这里主菜单让它成为成员变量
点击事件中,对主菜单需要额外考虑,所以这里判断点击的是不是主菜单,如果是的话,根据现在是展开状态还是收缩状态进行对应的动画效果
/**
* 打开菜单
*/
public void openMenu() {
changeState(true);
isOpen = true;
}
/**
* 关闭菜单
*/
public void closeMenu() {
changeState(false);
isOpen = false;
}
这两个方法是展开菜单和收缩菜单的方法,定义成了public,所以以后用这个控件的时候还能用代码控制展开和收缩
/**
* 弹出动画和收回的动画
*/
private RotateAnimation toAnimation = RotateAnimationUtil.rotateSelf(0, 360, animationDuration);
private RotateAnimation backAnimation = RotateAnimationUtil.rotateSelf(360, 0, animationDuration);
private MyListener myListener = new MyListener();
/**
* 实现了动画的监听接口的类,AnimationListenerAdapter是一个适配器,
* 也就是实现了一个接口中的所有方法,方法中都不写具体代码,
* 供给子类继承的时候可选择性的重写某个方法
*/
private class MyListener extends AnimationListenerAdapter {
public boolean isClose;
@Override
public void onAnimationEnd(Animation animation) {
int childCount = getChildCount();
for (int i = childCount - 1; i > 0; i--) {
View view = getChildAt(i);
if (isClose) { // 如果要展开
view.setVisibility(View.VISIBLE);
} else {
view.clearAnimation();
view.setVisibility(View.INVISIBLE);
}
}
if (stateAnimationListener != null) {
stateAnimationListener.animationEnd(isClose);
}
}
}
/**
* 弹出或者收起菜单
*/
public void changeState(final boolean isClose) {
toAnimation.setAnimationListener(myListener);
backAnimation.setAnimationListener(myListener);
myListener.isClose = isClose;
if (!isClose) {
menu.startAnimation(backAnimation);
} else {
menu.startAnimation(toAnimation);
}
int childCount = getChildCount();
for (int i = 1; i < childCount; i++) {
final View view = getChildAt(i);
if (isClose) { // 如果展开
view.setEnabled(true);
TranslateAnimationUtil.translateSelfAbsolute(view, 0, 0, menu.getTop() - view.getTop(), 0, animationDuration);
} else {
view.setEnabled(false);
TranslateAnimationUtil.translateSelfAbsolute(view, 0, 0, 0, menu.getTop() - view.getTop(), animationDuration);
}
}
if (isClose) {
SmartMenu.this.setBackgroundColor(openColor);
} else {
SmartMenu.this.setBackgroundColor(closeColor);
}
}
上述代码是显示动画的关键代码,其实代码不难,这里对几个不太容易懂的地方做一下解释:
a).由于动画的代码都几乎一样,只有参数不一样,所以这里是用了一个以前自己封装的一个动画的类,一句话可以实现动画或者返回一个动画
b).类MyListener主要是根据类中的成员变量isClose,在动画的结束的时候显示或者隐藏几个小菜单
类AnimationListenerAdapter是一个典型的适配器
/**
* 动画接口的适配器
*
* @author xiaojinzi
*
*/
public class AnimationListenerAdapter implements AnimationListener {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
}
动画基本上就完成了,但是我们还需要在点击事件的时候告知用户点击的是哪个控件以及对应的下标
所以我们就需要定义一个供用户设置监听的接口
/**
* 小菜单的点击事件
*/
public interface OnClickListener {
/**
* 回调的方法
*
* @param v 被点击的小菜单
* @param position 点击的是第几个菜单
*/
public void click(View v, int position);
}
然后声明一个此接口的成员变量,在点击事件的时候回调这个接口
所以完整代码是:
@Override
public void onClick(View v) {
if (v == menu) {
if (isOpen) {
closeMenu();
} else {
openMenu();
}
} else {
//点击了小菜单肯定是展开状态所以需要关闭菜单
closeMenu();
//如果用户没有设置小菜单的监听,那么就直接返回
if (onClickListener == null)
return;
//获取孩子的个数
int childCount = getChildCount();
//循环找出用户点击的小菜单,然后回调接口
for (int i = 1; i < childCount; i++) {
View view = getChildAt(i);
if (v == view) {
onClickListener.click(v, i);
}
}
}
}
最后就是处理一下触摸冲突的问题,由于我们的控件是填充父容器的,所以事件冲突在所难免
我们需要重写onTouchEvent方法来处理一下
@Override
public boolean onTouchEvent(MotionEvent e) {
if (isOpen) {
closeMenu();
return true;
}
return super.onTouchEvent(e);
}
我们通过判断当前的状态判断是否是展开状态,如果是展开状态,因为是覆盖的效果,所以不能让它点击到覆盖下方的控件,所以这里要return true;表示事件到这里就结束了,不会在传递给被覆盖的那些控件了,并且关闭我们的菜单,这样子就达到了弹出的时候点击任何地方都会收缩菜单的效果
如果不是展开状态,就让父类去处理.
弹出式小控件到现在全部完工,下面提供代码的下载链接: