无聊刷帖看到一个求助,试着写了一下。
一个自定义Switch控件,附带动画效果。
说是控件,其实是一个布局容器,先上效果图:
先讲原理,再看高清源码。
原理:
好像没啥原理,汗...
跟其它自定义容器控件一样,一般要注意:
(1)计算好大小,宽度和高度
(2)计算好子View的布局位置
不是一般要注意的:
(3)动画是用的nineoldandroids
(4)遮挡效果是通过控制子View的绘制顺序
高清源码:
(1)计算大小:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 初始化,只调用一次 if (!isInit) { childLeft = getChildAt(0); childRight = getChildAt(1); childLeft.setOnClickListener(leftClickListener); childRight.setOnClickListener(rightClickListener); measureChild(childLeft, widthMeasureSpec, heightMeasureSpec); measureChild(childRight, widthMeasureSpec, heightMeasureSpec); // 记录childLeft和childRight的宽高 leftChildW = childLeft.getMeasuredWidth(); leftChildH = childLeft.getMeasuredHeight(); rightChildW = childRight.getMeasuredWidth(); rightChildH = childRight.getMeasuredHeight(); // 初始化动画 smallToBig = ObjectAnimator.ofFloat(null, "scaleY", new float[] { 0.8f, 1 }); bigToSmall = ObjectAnimator.ofFloat(null, "scaleY", new float[] { 1, 0.8f }); leftToRightL = ObjectAnimator.ofFloat(childLeft, "translationX", new float[] { 0, leftChildW / 2 }); rightToLeftL = ObjectAnimator.ofFloat(childLeft, "translationX", new float[] { 0 }); leftToRightR = ObjectAnimator.ofFloat(childRight, "translationX", new float[] { 0, rightChildW / 2 }); rightToLeftR = ObjectAnimator.ofFloat(childRight, "translationX", new float[] { 0 }); animatorSet = new AnimatorSet(); animatorSet.addListener(mAnimatorListener); isInit = true; } // 宽度为两个child宽度相加 widthMeasureSpec = MeasureSpec.makeMeasureSpec(leftChildW + rightChildW, MeasureSpec.EXACTLY); // 高度为两个child中较高的那个 heightMeasureSpec = MeasureSpec.makeMeasureSpec(leftChildH > rightChildH ? leftChildH : rightChildH, MeasureSpec.EXACTLY); setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); }
看起来有点长,其实真正计算大小的就几句,其它的时其它东西的初始化,因为有些东西(比如动画)初始化需要子View的宽高,所以也放在了这里,我也不知道要放哪里好,加一个boolean变量isInit来控制动画之类的东西只会进行一次初始化,onMeasure和onLayout的调用频率非常高,应该避免在这两个方法内进行大量重复new之类的操作。
计算大小没什么好说的,主要注意一个自定义View时经常用到的方法:
measureChild(childLeft, widthMeasureSpec, heightMeasureSpec); measureChild(childRight, widthMeasureSpec, heightMeasureSpec);
measureChild方法可以让你手动去测量一下child,得到child的测量宽高。
注意是测量宽高,不是最终的真实宽高,getMeasuredWidth()和getWidth()是不同的。
(2)布局子View
protected void onLayout(boolean changed, int l, int t, int r, int b) { switch (currentState) { // 两个child的移动是通过动画,并不需要在这里进行特别处理,注意动画的参数就行了 case STATE_LEFT_ON_TOP: case STATE_RIGHT_ON_TOP: childLeft.layout(0, 0, leftChildW, leftChildH); childRight.layout(leftChildW - rightChildW / 2, 0, leftChildW + rightChildW / 2, rightChildH); break; } // 这里是初始时的状态判断并初始化显示 if (!isInit2) { if (currentState == STATE_LEFT_ON_TOP) { // 只需要把childRight变小 bigToSmall.setTarget(childRight); bigToSmall.start(); } else { // 把childLeft变小并且两个都向右移 bigToSmall.setTarget(childLeft); animatorSet = null; animatorSet = new AnimatorSet(); animatorSet.addListener(mAnimatorListener); animatorSet.playTogether(bigToSmall, leftToRightL, leftToRightR); animatorSet.start(); } isInit2 = true; } }
布局也没什么好说的,注释写得也清楚。
像注释说得,两种状态下并不用分情况进行布局,始终是靠左进行布局就行了,移动是动画做的事情,多试几遍动画的参数,看看那个行就OK了。
这里isInit2同理isInit,防止重复。
if(isInit2)里的内容是修改初始时的显示,如果没有这个,不管什么状态,初始时都是这样的:
(3)动画实现
public void changeState() { if (!isAniming) { if (currentState == STATE_RIGHT_ON_TOP) { currentState = STATE_LEFT_ON_TOP; if (mStateChangeListener != null) { mStateChangeListener.onStateChange(currentState); } smallToBig.setTarget(childLeft); bigToSmall.setTarget(childRight); animatorSet.playTogether(smallToBig, bigToSmall, rightToLeftL, rightToLeftR); animatorSet.start(); } else { currentState = STATE_RIGHT_ON_TOP; if (mStateChangeListener != null) { mStateChangeListener.onStateChange(currentState); } smallToBig.setTarget(childRight); bigToSmall.setTarget(childLeft); animatorSet.playTogether(smallToBig, bigToSmall, leftToRightL, leftToRightR); animatorSet.start(); } } }
这里动画用了nineoldandroids来实现。
前面在onMeasure方法里已经进行了动画的初始化,这里只要判断一下要执行那些动画,然后start()就行了。
比较难的是动画初始化时的参数,我的经验就是“试”,多试几次就行了。
(4)遮挡效果
@Override protected int getChildDrawingOrder(int childCount, int i) { switch (currentState) { case STATE_LEFT_ON_TOP: // childLeft在上,需要先draw childRight再draw childLeft // 后draw的会覆盖先draw的,这样childLeft才会在上层 if (i == 0) { return 1; } else { return 0; } default: return i; } }
这个方法是重写父类ViewGroup的,重写这个方法可以控制child的绘制顺序,记住后绘制的会遮挡住先绘制的。
注意这个方法要生效要先调用另一个方法:
setChildrenDrawingOrderEnabled(true);// 开启有序绘制child
利用ViewGroup这个特性可以实现挺多有趣效果的,比如多个child相互遮挡,点击那个就顶层显示,也可以不断切换绘制顺序来实现类似轮播的效果。
这样,一个自定义Switch控件就完成了。
前面说过,其实这个是个布局容器。最前面的效果图就是我放了两个TextView在里面形成的效果,放其它View也是可以的。
<view android:id="@+id/switchView" android:layout_width="wrap_content" android:layout_height="wrap_content" class="jjj.demo.switchviewdemo.SwitchView" > <TextView android:layout_width="60dp" android:layout_height="40dp" android:background="@drawable/shape_corner_red" android:gravity="center" android:text="ON" android:textColor="#ffffff" android:textSize="16sp" /> <TextView android:layout_width="60dp" android:layout_height="40dp" android:background="@drawable/shape_corner_blue" android:gravity="center" android:text="OFF" android:textColor="#ffffff" android:textSize="16sp" /> </view>
不知道为什么自定义控件在低版本系统上用下面这种方式经常不能正常显示:
<jjj.demo.switchviewdemo.SwitchView ></jjj.demo.switchviewdemo.SwitchView>
所以只能用上面<view ></view>这种方式,不知道只是模拟器的问题还是真机也会。
如果不要位移动画的话实现起来会更简单一点,对自定义控件不熟悉的可以去试着写一下。
感觉写得问题挺多的,有好的建议跪求指点评论啊。