以前做这样的导航栏控件使用继承View实现的,导航个数写死例如5个,不断的计算变化前后的各个区域的Rect,重写onTouchEvent,根据点击点不同进行重新绘制,以前能实现的效果,只是图片的上移和长图的动态移动,动画效果并不理想。在使用的过程中,如果要增加导航图片数量,计算很麻烦。因此一直想抽空,改一改这个自定义控件。
上面效果图用的方法是:1.继承自ViewGroup,2.利用属性动画。
首先分析实现原理:从效果图可以看出基本的图片包括导航图片(选中/未选中)、整体背景图片、选中的覆盖图片、各图标之间的分割图片,具体如下。
我进行这样自定义控件设置的时候,第一步首先把背景、分割图片、选中后的覆盖图片绘制出来。因此先自定义这三个属性 attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BottomWolfKillNavigation">
<attr name="backgroundimage" format="reference"></attr>
<attr name="frontimage" format="reference"></attr>
<attr name="cutimage" format="reference"></attr>
</declare-styleable>
</resources>
上面的属性值分别代表背景、选中的覆盖图、分割图。
在测试的布局文件中需要设定这三个属性值
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="80dp"
android:id="@+id/btn2"
android:text="2个图标"/>
<Button
android:layout_width="match_parent"
android:layout_height="80dp"
android:id="@+id/btn3"
android:text="3个图标"/>
<Button
android:layout_width="match_parent"
android:layout_height="80dp"
android:id="@+id/btn4"
android:text="4个图标"/>
<Button
android:layout_width="match_parent"
android:layout_height="80dp"
android:id="@+id/btn5"
android:text="5个图标"/>
</LinearLayout>
<com.xsl.widget.navigation.BottomWolfKillNavigation
android:id="@+id/nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:backgroundimage="@drawable/img_com_tab_bg"
app:frontimage="@drawable/img_com_tab_pressed_new_x"
app:cutimage="@drawable/img_home_bottom_item_line"
></com.xsl.widget.navigation.BottomWolfKillNavigation>
</RelativeLayout>
在自定义的ViewGroup中绘制
public class BottomWolfKillNavigation extends ViewGroup implements View.OnClickListener{
//导航栏背景图片
private int backgroundImageRes;
//导航栏选择后的前图
private int frontImageRes;
//导航栏的分割图
private int cutImageRes;
private Paint mPaint;
public BottomWolfKillNavigation(Context context) {
this(context,null);
}
public BottomWolfKillNavigation(Context context, AttributeSet attrs) {
//super(context, attrs);
this(context,attrs,0);
}
public BottomWolfKillNavigation(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
parseAttrs(context, attrs);
initView();
}
//获取参数值
private void parseAttrs(Context context,AttributeSet attrs){
if (attrs!=null){
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BottomWolfKillNavigation,0,0);
backgroundImageRes = typedArray.getResourceId(R.styleable.BottomWolfKillNavigation_backgroundimage,0);
frontImageRes = typedArray.getResourceId(R.styleable.BottomWolfKillNavigation_frontimage,0);
cutImageRes = typedArray.getResourceId(R.styleable.BottomWolfKillNavigation_cutimage,0);
typedArray.recycle();
}
}
private void initView(){
mPaint = new Paint();
mPaint.setAntiAlias(false);
//ViewGroup默认不会调用onDraw方法设置背景色后会调用
setBackgroundColor(Color.TRANSPARENT);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(Color.TRANSPARENT);
Rect transRect = new Rect(0,0,ScreenUtils.getScreenWidth(),SizeUtils.dp2px(20));
canvas.drawRect(transRect,mPaint);
mPaint.reset();
Rect backgroundRect = new Rect(0,SizeUtils.dp2px(20),ScreenUtils.getScreenWidth(),SizeUtils.dp2px(80));
Bitmap background = BitmapFactory.decodeResource(getResources(),backgroundImageRes);
canvas.drawBitmap(background,null,backgroundRect,mPaint);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//设置导航栏的宽度、高度
setMeasuredDimension(ScreenUtils.getScreenWidth(), SizeUtils.dp2px(80));
//需要适配虚拟的导航按键
if (ScreenUtils.isPortrait()){
}else{
}
}
}
通过以上可以画出背景,上面用到的获取屏幕宽度的工具类是在项目中引用第三方工具类implementation "com.blankj:utilcode:1.12.5"。链接地址
http://www.apkbus.com/blog-901770-76998.html。
为了能够设置每个导航控件的定义一个Item存选中图片与未选中图标
public class BottomImageItem {
private int unSelectedImageRes;//没有选择的图片
private int selectedImageRes;//选择后的图片
public BottomImageItem(@DrawableRes int unSelectedImageRes, @DrawableRes int selectedImageRes) {
this.unSelectedImageRes = unSelectedImageRes;
this.selectedImageRes = selectedImageRes;
}
public int getUnSelectedImageRes() {
return unSelectedImageRes;
}
public void setUnSelectedImageRes(int unSelectedImageRes) {
this.unSelectedImageRes = unSelectedImageRes;
}
public int getSelectedImageRes() {
return selectedImageRes;
}
public void setSelectedImageRes(int selectedImageRes) {
this.selectedImageRes = selectedImageRes;
}
}
自定义控件的重写方法的顺序 onMeasure onLayout onDraw
onMeasure中已经指定宽高,高度为80dp,其中0-20dp透明,20-80dp是背景图。
为了能够动态设置导航图标的数量,需要有个集合还管理导航图标
//存导航栏每个Item
private ArrayList<BottomImageItem> bottomImageItemsList = new ArrayList<>();
//存分割线
private ArrayList<ImageView> cutImageViewList = new ArrayList<>();
//存导航图标
private ArrayList<ImageView> iconImageViewList = new ArrayList<>();
//暴露给外界的接口,用于添加导航图片
public BottomWolfKillNavigation addBottomImageItem(BottomImageItem item){
bottomImageItemsList.add(item);
ImageView cutImage = new ImageView(context);
this.addView(cutImage);
if (iconImageViewList.size()>0){
cutImageViewList.add(cutImage);
Log.i("xsl","cutlinesize="+cutImageViewList.size());
}
ImageView imageView = new ImageView(context);
//为了能设置监听获取点击的是哪个view
imageView.setTag(bottomImageItemsList.size()-1);
iconImageViewList.add(imageView);
this.addView(imageView);
return this;
}
还需要暴露给外界设置该ViewGroup各个孩子的方法
public void initialise() {
itemSize = bottomImageItemsList.size();
//导航栏图片数量+分割线数量+前面选择图片
//分割线数量等于导航栏图片数量-1
//前面选择图片数量等于1
screenWidth = ScreenUtils.getScreenWidth();
cutWidth = SizeUtils.dp2px(2);
//除去分割线的宽度
contentWidth = screenWidth-cutWidth*(itemSize-1);
//平均每个导航栏的宽度
everyWidth = (int)(contentWidth/(itemSize+0.5f));
//被选中的导航栏的宽度
longWidth = contentWidth-everyWidth*(itemSize-1);
top = SizeUtils.dp2px(20);
bottom = SizeUtils.dp2px(80);
bigImageHalfWidth = SizeUtils.dp2px(30);
smallImageHalfWidth = SizeUtils.dp2px(25);
cutMoveLength = longWidth-everyWidth;
Log.i("xsl","原始计算值"+longWidth+","+everyWidth);
//drawContent();
for(int i=0;i<getChildCount();i++){
if (i!=0){
if (i%2==0){
((ImageView)getChildAt(i)).setImageResource(cutImageRes);
((ImageView)getChildAt(i)).setScaleType(ImageView.ScaleType.FIT_XY);
}else{
if (i==1){
((ImageView)getChildAt(i)).setImageResource(bottomImageItemsList.get(i/2).getSelectedImageRes());
}else{
((ImageView)getChildAt(i)).setImageResource(bottomImageItemsList.get(i/2).getUnSelectedImageRes());
}
//设置了图标的监听
(getChildAt(i)).setOnClickListener(this);
((ImageView)getChildAt(i)).setScaleType(ImageView.ScaleType.FIT_XY);
}
}else{
frontImageView = (ImageView) getChildAt(i);
((ImageView)getChildAt(i)).setImageResource(frontImageRes);
((ImageView)getChildAt(i)).setScaleType(ImageView.ScaleType.FIT_XY);
}
}
}
经过上面两个方法其实,我进行设置绘制child的顺序是选中的背景图片、导航图片、分割图片、导航图片、分割图片……一开始我是把选中的背景图片放在最后绘制,结果图标被覆盖,色调变了。
其实onMeasure之后就应该讲onLayout,先贴源码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i=0;i<itemSize*2;i++){
View view = getChildAt(i);
//单数是图片,双数是分割符,0是选中的前景图片
if(i==0){
view.layout(0,top,longWidth,bottom);
}else{
if (i%2==0){
//分割线
view.layout(longWidth + ((i-1)/ 2) * (everyWidth + cutWidth), top, longWidth + ((i -1)/ 2) * (everyWidth + cutWidth) + cutWidth, bottom);
}else{
//导航图片
int position =(i-1)/2;
int left = 0;
int right = 0;
if(position==0) {
left = 0;
right = longWidth;
}else{
left = longWidth+cutWidth*position+everyWidth*(position-1);
right = longWidth+cutWidth*position+everyWidth*(position);
}
int center = (left+right)/2;
if(position==0){
view.layout(center-bigImageHalfWidth,0,center+bigImageHalfWidth,bigImageHalfWidth*2);
// view.layout(center-smallImageHalfWidth,SizeUtils.dp2px(25),center+smallImageHalfWidth,SizeUtils.dp2px(25)+smallImageHalfWidth*2);
}else{
view.layout(center-smallImageHalfWidth,SizeUtils.dp2px(25),center+smallImageHalfWidth,SizeUtils.dp2px(25)+smallImageHalfWidth*2);
}
}
}
}
}
目前的onlayout是按第0个位置被选中进行布局的。
下面的任务就是要在点击图标(给孩子设置监听)的时候让所有的组件按想要的方式动起来:
1.选中的背景图片移动
2.分割图片的移动
3.导航图片的缩放及移动
@Override
public void onClick(View v) {
selectPosition = (Integer) v.getTag();
if (selectPosition!=oldPosition){
iconImageViewList.get(oldPosition).setImageResource(bottomImageItemsList.get(oldPosition).getUnSelectedImageRes());
//前景选中图片的动画
frontImageAnimation();
}
}
前景选中图片的动画函数
private void frontImageAnimation(){
float begin = 0f;
float length = 0f;
float end = 0f;
//总共移动距离
length = (selectPosition-oldPosition)*(everyWidth+cutWidth);
//左边的开始点
begin = (oldPosition)*(everyWidth+cutWidth);
end = begin+length;
final ObjectAnimator animator = ObjectAnimator.ofFloat(frontImageView,"translationX",begin,end);
animator.setDuration(500);
animator.start();
//设置动画监听移动的同时,分割图片、导航图片要变化
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
//动画开始后要进行分割图片的动画下面有代码
//导航图片的动画
}
@Override
public void onAnimationEnd(Animator animation) {
//动画结束
oldPosition = selectPosition;
animator.removeAllListeners();
//设置选中的图标
iconImageViewList.get(selectPosition).setImageResource(bottomImageItemsList.get(selectPosition).getSelectedImageRes());
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
那么在前景选中图片移动的同时,分割图片也要移动这里要注意到跨距离点击,分割图片是一个一个移动的。具体代码如下:
//用于分割线动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(500/(Math.abs(selectPosition-oldPosition)));
List<Animator> animatorList = new ArrayList<>();
if (oldPosition<selectPosition){
for (int i=oldPosition;i<selectPosition;i++){
View x = cutImageViewList.get(i);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(x,"translationX",
0,-cutMoveLength);
animatorList.add(objectAnimator);
}
animatorSet.playSequentially(animatorList);
animatorSet.start();
//点击右边的,图片左移动
iconAnimationToRight(oldPosition,selectPosition);
}else{
for (int i=oldPosition;i>selectPosition;i--){
View x = cutImageViewList.get(i-1);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(x,"translationX",
-cutMoveLength,0);
animatorList.add(objectAnimator);
}
animatorSet.playSequentially(animatorList);
animatorSet.start();
//这是图标缩放移动的函数
iconAnimationToLeft(oldPosition,selectPosition);
}
这里要注意设置的时间
animatorSet.setDuration(500/(Math.abs(selectPosition-oldPosition)));
启动动画的方式
animatorSet.playSequentially(animatorList);
还有重要的两个函数就是图标的移动和缩放
iconAnimationToRight(oldPosition,selectPosition);
iconAnimationToLeft(oldPosition,selectPosition);
我在分析缩放平移的时候首先是分析两个相邻的之间的缩放
一个图片的动画就包括XY缩放XY平移因此用AnimatorSet.playTogether方法实现,关键跨距离时,中间的经过的各个导航图标都要缩放,因此同样需要设置某个动画监听,结束动画后,继续回调该方法,具体源码
private void iconAnimationToRight(int i, int selectPosition){
List<Animator> animatorListSmallScale = new ArrayList<>();
//用于图片平移缩放动画
AnimatorSet animatorSetSmallScale = new AnimatorSet();
animatorSetSmallScale.setDuration(500/(Math.abs(selectPosition-oldPosition)));
ObjectAnimator objectAnimator1 = null;
if (i==0) {
objectAnimator1 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"scaleX",
1f, 1f/1.2f);
ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"scaleY",
1f, 1f/1.2f);
ObjectAnimator objectAnimator3 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"translationX",
0, -((longWidth - everyWidth) / 2));
ObjectAnimator objectAnimator4 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"translationY",
0,SizeUtils.dp2px(18));
ObjectAnimator objectAnimator5 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"scaleX",
1f, 1.2f);
ObjectAnimator objectAnimator6 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"scaleY",
1f, 1.2f);
ObjectAnimator objectAnimator7 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"translationX",
0, -((longWidth - everyWidth) / 2));
ObjectAnimator objectAnimator8 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"translationY",
0, -SizeUtils.dp2px(15));
animatorListSmallScale.add(objectAnimator1);
animatorListSmallScale.add(objectAnimator2);
animatorListSmallScale.add(objectAnimator3);
animatorListSmallScale.add(objectAnimator4);
animatorListSmallScale.add(objectAnimator5);
animatorListSmallScale.add(objectAnimator6);
animatorListSmallScale.add(objectAnimator7);
animatorListSmallScale.add(objectAnimator8);
}else{
objectAnimator1 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"scaleX",
1.2f, 1f);
ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"scaleY",
1.2f, 1f);
ObjectAnimator objectAnimator3 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"translationX",
-((longWidth - everyWidth) / 2),-(longWidth - everyWidth));
ObjectAnimator objectAnimator4 = ObjectAnimator.ofFloat(iconImageViewList.get(i),
"translationY",
-SizeUtils.dp2px(15),0);
ObjectAnimator objectAnimator5 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"scaleX",
1f, 1.2f);
ObjectAnimator objectAnimator6 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"scaleY",
1f, 1.2f);
ObjectAnimator objectAnimator7 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"translationX",
0, -((longWidth - everyWidth) / 2));
ObjectAnimator objectAnimator8 = ObjectAnimator.ofFloat(iconImageViewList.get(i + 1),
"translationY",
0, -SizeUtils.dp2px(15));
animatorListSmallScale.add(objectAnimator1);
animatorListSmallScale.add(objectAnimator2);
animatorListSmallScale.add(objectAnimator3);
animatorListSmallScale.add(objectAnimator4);
animatorListSmallScale.add(objectAnimator5);
animatorListSmallScale.add(objectAnimator6);
animatorListSmallScale.add(objectAnimator7);
animatorListSmallScale.add(objectAnimator8);
}
animatorSetSmallScale.playTogether(animatorListSmallScale);
animatorSetSmallScale.start();
i++;
final int x=i;
final int selectflag = selectPosition;
objectAnimator1.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (x<selectflag){
iconAnimationToRight(x,selectflag);
}
//objectAnimator1.removeAllListeners();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
点击选中的左边同样的逻辑
为了能测试动态改变图标数量还需要暴露接口
public void clear(){
cutImageViewList.clear();
iconImageViewList.clear();
bottomImageItemsList.clear();
oldPosition=selectPosition=0;
removeAllViews();
}
最后贴出测试代码
public class WolfActivity extends Activity implements View.OnClickListener{
private BottomWolfKillNavigation navigation;
private Button bt2;
private Button bt3;
private Button bt4;
private Button bt5;
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn2:
navigation.clear();
navigation.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_club_normal,R.drawable.icon_home_club_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_game_normal,R.drawable.icon_home_game_pressed))
.initialise();
break;
case R.id.btn3:
navigation.clear();
navigation.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_club_normal,R.drawable.icon_home_club_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_game_normal,R.drawable.icon_home_game_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_shop_normal,R.drawable.icon_home_shop_pressed))
.initialise();
break;
case R.id.btn4:
navigation.clear();
navigation.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_club_normal,R.drawable.icon_home_club_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_game_normal,R.drawable.icon_home_game_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_shop_normal,R.drawable.icon_home_shop_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_social_normal,R.drawable.icon_home_social_pressed))
.initialise();
break;
case R.id.btn5:
navigation.clear();
navigation.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_club_normal,R.drawable.icon_home_club_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_game_normal,R.drawable.icon_home_game_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_shop_normal,R.drawable.icon_home_shop_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_social_normal,R.drawable.icon_home_social_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_watch_normal,R.drawable.icon_home_watch_pressed))
.initialise();
break;
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Utils.init(getApplication());
setContentView(R.layout.activity_wolf);
navigation = (BottomWolfKillNavigation) findViewById(R.id.nav);
navigation.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_club_normal,R.drawable.icon_home_club_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_game_normal,R.drawable.icon_home_game_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_shop_normal,R.drawable.icon_home_shop_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_social_normal,R.drawable.icon_home_social_pressed))
.addBottomImageItem(new BottomImageItem(R.drawable.icon_home_watch_normal,R.drawable.icon_home_watch_pressed))
.initialise();
bt2 = findViewById(R.id.btn2);
bt3 = findViewById(R.id.btn3);
bt4 = findViewById(R.id.btn4);
bt5 = findViewById(R.id.btn5);
bt2.setOnClickListener(this);
bt3.setOnClickListener(this);
bt4.setOnClickListener(this);
bt5.setOnClickListener(this);
}
}