Android自定义控件——仿优酷圆盘菜单

最近学习的时候,看见一份资料上教怎么写自定义控件,上面的示例用的是优酷早期版本的客户端,该客户端的菜单就是一个自定义的组件(现在的版本就不清楚有没有了,没下载过了),好吧,废话不多说,先上优酷的原型图。


这个自定义组件感官上看是,里外三层设计,每一层上有布置不同的菜单按钮,每一层又设置了进入和退出的动画,来增强用户的体验效果。这种设计非常好,简洁美观,以下是我仿照优酷菜单自定义的一个组件,写的马马虎虎,仅作为参考。

1,首先,里外三层看似错乱,其实无外乎就是UI布局,并不高深。以下是布局文件:

[html]  view plain copy print ?
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context=".MainActivity" >  
  6.   
  7.     <RelativeLayout  
  8.         android:id="@+id/rl_level1"  
  9.         android:layout_width="100dip"  
  10.         android:layout_height="50dip"  
  11.         android:layout_alignParentBottom="true"  
  12.         android:layout_centerHorizontal="true"  
  13.         android:background="@drawable/level1" >  
  14.   
  15.         <ImageButton  
  16.             android:id="@+id/ib_home"  
  17.             android:layout_width="wrap_content"  
  18.             android:layout_height="wrap_content"  
  19.             android:layout_centerInParent="true"  
  20.             android:background="@drawable/icon_home" />  
  21.     </RelativeLayout>  
  22.   
  23.     <RelativeLayout  
  24.         android:id="@+id/rl_level2"  
  25.         android:layout_width="200dip"  
  26.         android:layout_height="100dip"  
  27.         android:layout_alignParentBottom="true"  
  28.         android:layout_centerHorizontal="true"  
  29.         android:background="@drawable/level2" >  
  30.   
  31.         <ImageButton  
  32.             android:layout_width="wrap_content"  
  33.             android:layout_height="wrap_content"  
  34.             android:layout_alignParentBottom="true"  
  35.             android:layout_marginBottom="5dip"  
  36.             android:layout_marginLeft="10dip"  
  37.             android:background="@drawable/icon_search" />  
  38.   
  39.         <ImageButton  
  40.             android:id="@+id/ib_menu"  
  41.             android:layout_width="wrap_content"  
  42.             android:layout_height="wrap_content"  
  43.             android:layout_centerHorizontal="true"  
  44.             android:layout_marginTop="10dip"  
  45.             android:background="@drawable/icon_menu" />  
  46.   
  47.         <ImageButton  
  48.             android:layout_width="wrap_content"  
  49.             android:layout_height="wrap_content"  
  50.             android:layout_alignParentBottom="true"  
  51.             android:layout_alignParentRight="true"  
  52.             android:layout_marginBottom="5dip"  
  53.             android:layout_marginRight="10dip"  
  54.             android:background="@drawable/icon_myyouku" />  
  55.     </RelativeLayout>  
  56.   
  57.     <RelativeLayout  
  58.         android:id="@+id/rl_level3"  
  59.         android:layout_width="320dip"  
  60.         android:layout_height="160dip"  
  61.         android:layout_alignParentBottom="true"  
  62.         android:layout_centerHorizontal="true"  
  63.         android:background="@drawable/level3" >  
  64.   
  65.         <ImageButton  
  66.             android:id="@+id/ib_channel1"  
  67.             android:layout_width="wrap_content"  
  68.             android:layout_height="wrap_content"  
  69.             android:layout_alignParentBottom="true"  
  70.             android:layout_marginBottom="10dip"  
  71.             android:layout_marginLeft="15dip"  
  72.             android:background="@drawable/channel1" />  
  73.   
  74.         <ImageButton  
  75.             android:id="@+id/ib_channel2"  
  76.             android:layout_width="wrap_content"  
  77.             android:layout_height="wrap_content"  
  78.             android:layout_above="@id/ib_channel1"  
  79.             android:layout_marginBottom="20dip"  
  80.             android:layout_marginLeft="40dip"  
  81.             android:background="@drawable/channel2" />  
  82.   
  83.         <ImageButton  
  84.             android:layout_width="wrap_content"  
  85.             android:layout_height="wrap_content"  
  86.             android:layout_above="@id/ib_channel2"  
  87.             android:layout_marginBottom="15dip"  
  88.             android:layout_marginLeft="10dip"  
  89.             android:layout_toRightOf="@id/ib_channel2"  
  90.             android:background="@drawable/channel3" />  
  91.   
  92.         <ImageButton  
  93.             android:layout_width="wrap_content"  
  94.             android:layout_height="wrap_content"  
  95.             android:layout_centerHorizontal="true"  
  96.             android:layout_marginTop="10dip"  
  97.             android:background="@drawable/channel4" />  
  98.   
  99.         <ImageButton  
  100.             android:id="@+id/ib_channel7"  
  101.             android:layout_width="wrap_content"  
  102.             android:layout_height="wrap_content"  
  103.             android:layout_alignParentBottom="true"  
  104.             android:layout_alignParentRight="true"  
  105.             android:layout_marginBottom="10dip"  
  106.             android:layout_marginRight="15dip"  
  107.             android:background="@drawable/channel7" />  
  108.   
  109.         <ImageButton  
  110.             android:id="@+id/ib_channel6"  
  111.             android:layout_width="wrap_content"  
  112.             android:layout_height="wrap_content"  
  113.             android:layout_above="@id/ib_channel7"  
  114.             android:layout_alignParentRight="true"  
  115.             android:layout_marginBottom="20dip"  
  116.             android:layout_marginRight="40dip"  
  117.             android:background="@drawable/channel6" />  
  118.   
  119.         <ImageButton  
  120.             android:layout_width="wrap_content"  
  121.             android:layout_height="wrap_content"  
  122.             android:layout_above="@id/ib_channel6"  
  123.             android:layout_marginBottom="15dip"  
  124.             android:layout_marginRight="10dip"  
  125.             android:layout_toLeftOf="@id/ib_channel6"  
  126.             android:background="@drawable/channel5" />  
  127.     </RelativeLayout>  
  128.   
  129. </RelativeLayout>  

布局后的UI效果见图:


如上图所示的样子,布局完成了,接下来就是添加一些动态的效果了,效果描述是这样的:该菜单由内而外分别叫做“1级菜单”,“2级菜单”和“3级菜单”,1级菜单和2级菜单的中心位置的ImageButton用来控制整个菜单的动态效果。点击1级菜单时,若2级或者3级菜单处于显示状态,则隐藏2级和3级菜单,如果没有显示,则只显示出2级菜单。点击2级菜单的时候,只控制3级菜单的显示和隐藏。

这里所有的动态效果都是由自定义的旋转动画来实现,下面我们先完成这个自定义的旋转动画:

[java]  view plain copy print ?
  1. package com.example.youkumenu;  
  2.   
  3. import android.view.animation.Animation;  
  4. import android.view.animation.RotateAnimation;  
  5. import android.widget.RelativeLayout;  
  6.   
  7. public class AnimationUtils {  
  8.   
  9.     public static boolean isRunningAnimation = false// 记录动画是否在执行  
  10.   
  11.     /** 
  12.      * 旋转出去的动画 
  13.      *  
  14.      * @param layout 
  15.      *            执行动画的对象 
  16.      * @param startOffset 
  17.      *            延迟时间 
  18.      */  
  19.     public static void outRotateAnimation(RelativeLayout layout, long startOffset) {  
  20.         // 防止父控件中的子控件抢焦点能力强,而将子控件设置为不可用  
  21.         for (int i = 0; i < layout.getChildCount(); i++) {  
  22.             layout.getChildAt(i).setEnabled(false);  
  23.         }  
  24.   
  25.         RotateAnimation ra = new RotateAnimation( //  
  26.                 0.0f, // 旋转开始的角度  
  27.                 -180.0f, // 旋转结束的角度  
  28.                 RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标X轴的参照物  
  29.                 0.5f, // 相对于参照物X轴的百分比  
  30.                 RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标Y轴的参照物  
  31.                 1.0f // 相对于参照物Y轴的百分比  
  32.         );  
  33.         ra.setDuration(500);  
  34.         ra.setStartOffset(startOffset); // 动画延迟时间  
  35.         ra.setFillAfter(true);  
  36.         ra.setAnimationListener(new MyAnimationListener());  
  37.         layout.startAnimation(ra);  
  38.     }  
  39.   
  40.     /** 
  41.      * 旋转进来的动画 
  42.      *  
  43.      * @param layout 
  44.      *            执行动画的对象 
  45.      */  
  46.     public static void inRotateAnimation(RelativeLayout layout) {  
  47.         // 进来的时候,将所有的子控件设置为可用  
  48.         for (int i = 0; i < layout.getChildCount(); i++) {  
  49.             layout.getChildAt(i).setEnabled(false);  
  50.         }  
  51.   
  52.         RotateAnimation ra = new RotateAnimation( //  
  53.                 -180.0f, // 旋转开始的角度  
  54.                 0.0f, // 旋转结束的角度  
  55.                 RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标X轴的参照物  
  56.                 0.5f, // 相对于参照物X轴的百分比  
  57.                 RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标Y轴的参照物  
  58.                 1.0f // 相对于参照物Y轴的百分比  
  59.         );  
  60.         ra.setDuration(500);  
  61.         ra.setFillAfter(true);  
  62.         layout.startAnimation(ra);  
  63.     }  
  64.   
  65.     static class MyAnimationListener implements Animation.AnimationListener {  
  66.   
  67.         /** 
  68.          * 动画开始的时候执行 
  69.          */  
  70.         @Override  
  71.         public void onAnimationStart(Animation animation) {  
  72.             // TODO Auto-generated method stub  
  73.             isRunningAnimation = true;  
  74.         }  
  75.   
  76.         /** 
  77.          * 动画结束的时候执行 
  78.          */  
  79.         @Override  
  80.         public void onAnimationEnd(Animation animation) {  
  81.             // TODO Auto-generated method stub  
  82.             isRunningAnimation = false;  
  83.         }  
  84.   
  85.         /** 
  86.          * 动画重复执行的时候 
  87.          */  
  88.         @Override  
  89.         public void onAnimationRepeat(Animation animation) {  
  90.             // TODO Auto-generated method stub  
  91.   
  92.         }  
  93.   
  94.     }  
  95.   
  96. }  


下面是MainActivity的主要代码,这里控制菜单的动态变化:

[java]  view plain copy print ?
  1. package com.example.youkumenu;  
  2.   
  3. import android.os.Bundle;  
  4. import android.app.Activity;  
  5. import android.view.KeyEvent;  
  6. import android.view.View;  
  7. import android.view.View.OnClickListener;  
  8. import android.widget.RelativeLayout;  
  9.   
  10. public class MainActivity extends Activity implements OnClickListener {  
  11.   
  12.     private RelativeLayout rlLevel1;  
  13.     private RelativeLayout rlLevel2;  
  14.     private RelativeLayout rlLevel3;  
  15.     /** 记录3级菜单是否展示 */  
  16.     private boolean isDisplayLevel3 = true;  
  17.     /** 记录2级菜单是否展示 */  
  18.     private boolean isDisplayLevel2 = true;  
  19.     /** 记录1级菜单是否展示 */  
  20.     private boolean isDisplayLevel1 = true;  
  21.   
  22.     @Override  
  23.     protected void onCreate(Bundle savedInstanceState) {  
  24.         super.onCreate(savedInstanceState);  
  25.         setContentView(R.layout.activity_main);  
  26.         rlLevel1 = (RelativeLayout) findViewById(R.id.rl_level1);  
  27.         rlLevel2 = (RelativeLayout) findViewById(R.id.rl_level2);  
  28.         rlLevel3 = (RelativeLayout) findViewById(R.id.rl_level3);  
  29.   
  30.         findViewById(R.id.ib_home).setOnClickListener(this);  
  31.         findViewById(R.id.ib_menu).setOnClickListener(this);  
  32.   
  33.     }  
  34.   
  35.     @Override  
  36.     public void onClick(View v) {  
  37.         switch (v.getId()) {  
  38.         case R.id.ib_home:  
  39.             if (AnimationUtils.isRunningAnimation) // 当前动画正在执行的时候,不执行动画  
  40.                 return;  
  41.             if (isDisplayLevel2) {  
  42.                 // 2级菜单正在展示  
  43.                 long startOffset = 0// 旋转延时时间  
  44.                 if (isDisplayLevel3) {  
  45.                     // 3级菜单也在展示,先旋转出去3级菜单,再旋转出去2级菜单  
  46.                     AnimationUtils.outRotateAnimation(rlLevel3, startOffset);  
  47.                     startOffset += 200;  
  48.                     isDisplayLevel3 = !isDisplayLevel3;  
  49.                 }  
  50.                 AnimationUtils.outRotateAnimation(rlLevel2, startOffset);  
  51.             } else {  
  52.                 // 2级菜单没有展示,需要旋转进来  
  53.                 AnimationUtils.inRotateAnimation(rlLevel2);  
  54.             }  
  55.             isDisplayLevel2 = !isDisplayLevel2;  
  56.             break;  
  57.         case R.id.ib_menu:  
  58.             if (AnimationUtils.isRunningAnimation)  
  59.                 return;  
  60.             if (isDisplayLevel3) {  
  61.                 // 3级菜单正在展示,需要旋转出去  
  62.                 AnimationUtils.outRotateAnimation(rlLevel3, 0);  
  63.             } else {  
  64.                 // 3级菜单没有展示,需要旋转进来  
  65.                 AnimationUtils.inRotateAnimation(rlLevel3);  
  66.             }  
  67.             isDisplayLevel3 = !isDisplayLevel3;  
  68.             break;  
  69.         default:  
  70.             break;  
  71.         }  
  72.     }  
  73.   
  74.     /** 
  75.      * 菜单按钮的处理 
  76.      */  
  77.     @Override  
  78.     public boolean onKeyDown(int keyCode, KeyEvent event) {  
  79.         // TODO Auto-generated method stub  
  80.         if (keyCode == KeyEvent.KEYCODE_MENU) {  
  81.             if (AnimationUtils.isRunningAnimation)  
  82.                 return super.onKeyDown(keyCode, event);  
  83.             if (isDisplayLevel1) {  
  84.                 // 1级菜单旋转出去  
  85.                 long startOffset = 0// 记录延迟时间  
  86.                 if (isDisplayLevel2) {  
  87.                     // 2级菜单旋转出去  
  88.                     if (isDisplayLevel3) {  
  89.                         // 3级菜单先旋转出去  
  90.                         AnimationUtils.outRotateAnimation(rlLevel3, startOffset);  
  91.                         startOffset += 200;  
  92.                         isDisplayLevel3 = !isDisplayLevel3;  
  93.                     }  
  94.                     AnimationUtils.outRotateAnimation(rlLevel2, startOffset);  
  95.                     startOffset += 200// 延迟200ms  
  96.                     isDisplayLevel2 = !isDisplayLevel2;  
  97.                 }  
  98.                 AnimationUtils.outRotateAnimation(rlLevel1, startOffset);  
  99.             } else {  
  100.                 // 1级菜单旋转进来  
  101.                 AnimationUtils.inRotateAnimation(rlLevel1);  
  102.             }  
  103.             isDisplayLevel1 = !isDisplayLevel1;  
  104.         }  
  105.         return super.onKeyDown(keyCode, event);  
  106.     }  
  107. }  

以上是整个自定义组件的全部源代码,值得注意的是:

1,关于控件的焦点问题。

在布局中可以看到,我在这个相对布局RelativeLayout中使用的控件都是ImageButton,ImageButton有个显著地特点就是它抢焦点的能力特别强。所以为了各层次菜单在动画执行的过程中,用户点击了ImageButton按钮,就会发生事件响应。这种用户体验是极不可取的,那么该怎么解决这个问题。

诚然通过API查找发现,RelativeLayout继承于ViewGroup类

ViewGroup提供了遍历子元素的方法getChildAt(int index),所以,我们在Animation的动画旋转出去的方法中,遍历菜单上的所有子控件,并设置其为不可用;而在动画旋转进来的方法,遍历菜单上的所有子控件,并设置其可用,这样的矛盾就解决了。


2,关于快速点击菜单2次的问题,当快速点击菜单2次的时候,即第一次点击菜单,菜单还没有完全旋转出去或者旋转进来的时候,伴随着第二次点击菜单,这里执行相反的动画效果,即菜单还未完全出去或者进来的时候,中途被迫停止操作,取之而来的是相反的动作,即旋转进来或者旋转出去。那么,这个问题怎么解决?

通过动画的API,可以发现Android给我们提供的一个接口:

[java]  view plain copy print ?
  1. public static interface AnimationListener {  
  2.         /** 
  3.          * <p>Notifies the start of the animation.</p> 
  4.          * 
  5.          * @param animation The started animation. 
  6.          */  
  7.         void onAnimationStart(Animation animation);  
  8.   
  9.         /** 
  10.          * <p>Notifies the end of the animation. This callback is not invoked 
  11.          * for animations with repeat count set to INFINITE.</p> 
  12.          * 
  13.          * @param animation The animation which reached its end. 
  14.          */  
  15.         void onAnimationEnd(Animation animation);  
  16.   
  17.         /** 
  18.          * <p>Notifies the repetition of the animation.</p> 
  19.          * 
  20.          * @param animation The animation which was repeated. 
  21.          */  
  22.         void onAnimationRepeat(Animation animation);  
  23.     }  
通过这个接口,一目了然这些方法的使用,只要自定义一个实现类,实现接口的三个方法后,设置一个静态变量来记录当前动画是否在运行状态,给动画设置这个监听。

在MainActivity里,只要调用这个控制菜单动画的方法前,判断一下动画的运行状态,再做操作,这样问题就可以解决了。

以上是个人爱好,源码已经上传,欢迎大家一起交流学习。


源码请在这里下载


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值