標籤:android switch 自訂 動畫
無聊刷帖看到一個求助,試著寫了一下。
一個自訂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)遮擋效果
@Overrideprotected 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>這種方式,不知道只是模擬器的問題還是真機也會。
如果不要位移動畫的話實現起來會更簡單一點,對自訂控制項不熟悉的可以去試著寫一下。
感覺寫得問題挺多的,有好的建議跪求指點評論啊。
Demo下載
Android好奇寶寶_番外篇_看臉的世界_03