今天要给大家带来一个自定义控件,这个控件在平板qq HD中有放上效果图
我就截图了我的设备上的一张图,是弹出的状态.如果收起来的时候,覆盖的半透明的白色就会消失,并且弹出来的小菜单都会收回.这就是这个控件的一个简单的介绍,而今天就要带大家来完成这个控件.
先放上实现好的效果图:
从效果上可以看出我们需要实现以下功能:
1.菜单收起来的时候就是一个很普通的图片
2.菜单弹出的时候需要给所在的容器覆盖一层白色半透明的层
3.弹出和收回的过程中,主菜单有一个旋转的动画,其余的小菜单有一个平移动画
4.点击小菜单的时候能调用一个自定义的回调接口,通知用户当前点击是第几个孩子,供用户编写之后的逻辑性代码
以上就是这个控件需要有的功能.下面小金子带大家来实现这个控件.首先我们先分析一下:
原理:
a)其实这个控件是填充父容器的,收缩的时候背景颜色是完全透明的.所以你只能看到右下角的那个主菜单
b)展开的时候无非就是换了一层白色的半透明的颜色,所以看上去是覆盖的效果,下图红色框框包围的就是控件的大小
原理:
a)从图中可以看出,我们的自定义控件是有孩子的,所以这就需要我们继承ViewGroup来实现
b)既然有孩子,那控件就承担着安排孩子位置的任务,所以你展开的时候看到的主菜单和小菜单的呈线性排列其实是这个自定义控件安排的
原理:
a)收缩的时候,就只让主菜单显示,其余小菜单都隐藏,自身背景变成完全透明
b)展开的时候主菜单和小菜单们都显示,并且让自身背景变成白色半透明的
c)在展开和收缩的时候加上响应的动画即可
这个控件会把第一个孩子当成主菜单,剩余的孩子当成可以弹出的小菜单
分析完毕之后,下面来写我们的代码:
一.首先起一个名字,并且重写几个构造方法:
这里的写法几乎是每一个自定义控件都一样的写法了,
首先继承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); } } }
而我为了代码的清晰,计算所有孩子的位置信息我写了一个方法,就是代码中第一句所调用的方法,目的就是计算出所有孩子位置的信息并且放到集合中
<span style="font-size: 24px;"> </span><span style="font-size:14px;">/** * 计算需要的数据 */ 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); } }</span>
可以看到我们的主菜单显示在最右下角,那么它的位置是如何计算出来的呢?
同理我们算出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,所以以后用这个控件的时候还能用代码控制展开和收缩
<pre name="code" class="java"> /** * 弹出动画和收回的动画 */ 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); }
如果不是展开状态,就让父类去处理.
弹出式小控件到现在全部完工,下面提供代码的下载链接:
源码下载