Android开发中会经常和控件打交道,有时Android提供的控件未必能满足业务的需求,这个时候就需要我们实现自定义一些控件,今天先大致了解一下自定义控件的要求和实现的基本原理。
分类
Android开发中用到的自定义控件可以分为有三种:
- 组合控件
把几个常用控件组合起来,形成一个新的控件,相当于一个组团的意思。 - 扩展控件
对现有控件进行功能或者样式上的扩展,相当于给原先的控件添加装备。 - 完全自定义控件
功能样式都自行定义设计,可以实现想要的功能。
光说不练到底是不行的,我打算每个文章里面加上一个小例子来写,理解也更方便。
开始
我当初入门的时候,就是以早期的优酷菜单的作为练手的,现在就还拿这个来说吧!老规矩,先放一张效果图:
该菜单由内而外分别叫做“1级菜单”,“2级菜单”和“3级菜单”,1级菜单和2级菜单的中心位置的ImageButton用来控制整个菜单的动态效果。点击1级菜单时,若2级或者3级菜单处于显示状态,则隐藏2级和3级菜单,如果没有显示,则只显示出2级菜单。点击2级菜单的时候,只控制3级菜单的显示和隐藏。
布局
首先,让我们来开始愉快的画布局吧!
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<RelativeLayout
android:id="@+id/rl_level1"
android:layout_width="100dip"
android:layout_height="50dip"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="@drawable/level1" >
<ImageButton
android:id="@+id/ib_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/icon_home" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_level2"
android:layout_width="200dip"
android:layout_height="100dip"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="@drawable/level2" >
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="5dip"
android:layout_marginLeft="10dip"
android:background="@drawable/icon_search" />
<ImageButton
android:id="@+id/ib_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dip"
android:background="@drawable/icon_menu" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="5dip"
android:layout_marginRight="10dip"
android:background="@drawable/icon_myyouku" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_level3"
android:layout_width="320dip"
android:layout_height="160dip"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="@drawable/level3" >
<ImageButton
android:id="@+id/ib_channel1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dip"
android:layout_marginLeft="15dip"
android:background="@drawable/channel1" />
<ImageButton
android:id="@+id/ib_channel2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/ib_channel1"
android:layout_marginBottom="20dip"
android:layout_marginLeft="40dip"
android:background="@drawable/channel2" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/ib_channel2"
android:layout_marginBottom="15dip"
android:layout_marginLeft="10dip"
android:layout_toRightOf="@id/ib_channel2"
android:background="@drawable/channel3" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dip"
android:background="@drawable/channel4" />
<ImageButton
android:id="@+id/ib_channel7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="10dip"
android:layout_marginRight="15dip"
android:background="@drawable/channel7" />
<ImageButton
android:id="@+id/ib_channel6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/ib_channel7"
android:layout_alignParentRight="true"
android:layout_marginBottom="20dip"
android:layout_marginRight="40dip"
android:background="@drawable/channel6" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/ib_channel6"
android:layout_marginBottom="15dip"
android:layout_marginRight="10dip"
android:layout_toLeftOf="@id/ib_channel6"
android:background="@drawable/channel5" />
</RelativeLayout>
</RelativeLayout>
动画
因为动画都是靠自定义的旋转动画来实现的,而且用的地方也比较多,所以对代码进行了抽取,封装了一个工具类,暴露出转入以及转出两个方法,来供调用……
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.RelativeLayout;
public class AnimationUtils {
public static boolean isRunningAnimation = false; // 记录动画是否在执行
/**
* 旋转出去的动画
*
* @param layout
* 执行动画的对象
* @param startOffset
* 延迟时间
*/
public static void outRotateAnimation(RelativeLayout layout, long startOffset) {
// 防止父控件中的子控件抢焦点能力强,而将子控件设置为不可用
for (int i = 0; i < layout.getChildCount(); i++) {
layout.getChildAt(i).setEnabled(false);
}
RotateAnimation ra = new RotateAnimation( //
0.0f, // 旋转开始的角度
-180.0f, // 旋转结束的角度
RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标X轴的参照物
0.5f, // 相对于参照物X轴的百分比
RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标Y轴的参照物
1.0f // 相对于参照物Y轴的百分比
);
ra.setDuration(500);
ra.setStartOffset(startOffset); // 动画延迟时间
ra.setFillAfter(true);
ra.setAnimationListener(new MyAnimationListener());
layout.startAnimation(ra);
}
/**
* 旋转进来的动画
*
* @param layout
* 执行动画的对象
*/
public static void inRotateAnimation(RelativeLayout layout) {
// 进来的时候,将所有的子控件设置为可用
for (int i = 0; i < layout.getChildCount(); i++) {
layout.getChildAt(i).setEnabled(false);
}
RotateAnimation ra = new RotateAnimation( //
-180.0f, // 旋转开始的角度
0.0f, // 旋转结束的角度
RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标X轴的参照物
0.5f, // 相对于参照物X轴的百分比
RotateAnimation.RELATIVE_TO_SELF, // 旋转坐标Y轴的参照物
1.0f // 相对于参照物Y轴的百分比
);
ra.setDuration(500);
ra.setFillAfter(true);
layout.startAnimation(ra);
}
static class MyAnimationListener implements Animation.AnimationListener {
/**
* 动画开始的时候执行
*/
@Override
public void onAnimationStart(Animation animation) {
// TODO Auto-generated method stub
isRunningAnimation = true;
}
/**
* 动画结束的时候执行
*/
@Override
public void onAnimationEnd(Animation animation) {
// TODO Auto-generated method stub
isRunningAnimation = false;
}
/**
* 动画重复执行的时候
*/
@Override
public void onAnimationRepeat(Animation animation) {
// TODO Auto-generated method stub
}
}
}
代码控制
然后就是在代码中进行控制。当点击的时候判断当前菜单的出现状态。然后根据菜单状态的不同执行相应操作。
import android.os.Bundle;
import android.app.Activity;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.RelativeLayout;
public class MainActivity extends Activity implements OnClickListener {
private RelativeLayout rlLevel1;
private RelativeLayout rlLevel2;
private RelativeLayout rlLevel3;
/** 记录3级菜单是否展示 */
private boolean isDisplayLevel3 = true;
/** 记录2级菜单是否展示 */
private boolean isDisplayLevel2 = true;
/** 记录1级菜单是否展示 */
private boolean isDisplayLevel1 = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rlLevel1 = (RelativeLayout) findViewById(R.id.rl_level1);
rlLevel2 = (RelativeLayout) findViewById(R.id.rl_level2);
rlLevel3 = (RelativeLayout) findViewById(R.id.rl_level3);
findViewById(R.id.ib_home).setOnClickListener(this);
findViewById(R.id.ib_menu).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ib_home:
if (AnimationUtils.isRunningAnimation) // 当前动画正在执行的时候,不执行动画
return;
if (isDisplayLevel2) {
// 2级菜单正在展示
long startOffset = 0; // 旋转延时时间
if (isDisplayLevel3) {
// 3级菜单也在展示,先旋转出去3级菜单,再旋转出去2级菜单
AnimationUtils.outRotateAnimation(rlLevel3, startOffset);
startOffset += 200;
isDisplayLevel3 = !isDisplayLevel3;
}
AnimationUtils.outRotateAnimation(rlLevel2, startOffset);
} else {
// 2级菜单没有展示,需要旋转进来
AnimationUtils.inRotateAnimation(rlLevel2);
}
isDisplayLevel2 = !isDisplayLevel2;
break;
case R.id.ib_menu:
if (AnimationUtils.isRunningAnimation)
return;
if (isDisplayLevel3) {
// 3级菜单正在展示,需要旋转出去
AnimationUtils.outRotateAnimation(rlLevel3, 0);
} else {
// 3级菜单没有展示,需要旋转进来
AnimationUtils.inRotateAnimation(rlLevel3);
}
isDisplayLevel3 = !isDisplayLevel3;
break;
default:
break;
}
}
/**
* 菜单按钮的处理
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
if (keyCode == KeyEvent.KEYCODE_MENU) {
if (AnimationUtils.isRunningAnimation)
return super.onKeyDown(keyCode, event);
if (isDisplayLevel1) {
// 1级菜单旋转出去
long startOffset = 0; // 记录延迟时间
if (isDisplayLevel2) {
// 2级菜单旋转出去
if (isDisplayLevel3) {
// 3级菜单先旋转出去
AnimationUtils.outRotateAnimation(rlLevel3, startOffset);
startOffset += 200;
isDisplayLevel3 = !isDisplayLevel3;
}
AnimationUtils.outRotateAnimation(rlLevel2, startOffset);
startOffset += 200; // 延迟200ms
isDisplayLevel2 = !isDisplayLevel2;
}
AnimationUtils.outRotateAnimation(rlLevel1, startOffset);
} else {
// 1级菜单旋转进来
AnimationUtils.inRotateAnimation(rlLevel1);
}
isDisplayLevel1 = !isDisplayLevel1;
}
return super.onKeyDown(keyCode, event);
}
}
至此,大功告成!