Android 带滑动效果的bottomBar

2017/9/30 posted in  Android

看到一个bottomBar的设计,感觉很好看,于是把它实现了出来

20171104150978644456283.gif
20171104150978644456283.gif

可以看到这是一个常见的bottomBar
把它分解一下

  1. 一共有5个item,每个item的背景颜色不一样
  2. 点击item时,item是通过滑动来移动到相应的item上的,这个移动也不是简单的线性移动,而是带有粘性的.
  3. item移动时,item颜色的切换是有item之间过渡的,类似于加了一个遮罩
  4. 移到item时,item本身是伴随item的移动是有一个动画的.

根据我们的分解,一步一步解决问题

考虑到这是一个bottomBar,我选择了自定义ViewGroup来实现.因为用ViewGroup添加item会比较方便.

public class AnimationBottomBar extends ViewGroup {
    @Override
    protected void onDraw(Canvas canvas) {        
    super.onDraw(canvas);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }
}

另外item内的小动画我也选择用缩放的形式实现,所以个效果图会有一些出入

添加item

通常来说,一个item会有一个图标和简短的标题.
举个例子,就像是知乎,即刻下方的bottomBar一样

所以一个item内有也要有一个图标和一个标题

添加item的时候要足够方便,使用代码添加是个不错的选择,类似于这样mAnimationBottomBar.addItem(item).

我创建了一个简单的BottomItem类来包装item

public class BottomItem {
    int drawableRes;//图标资源
    String title;//标题
    public BottomItem(@DrawableRes int drawableRes,String title){
        this.drawableRes=drawableRes;
        this.title=title;
    }
}

添加item之后,我将添加的BottomItem保存到一个list里

public AnimationBottomBar addItem(BottomItem bottomItem) {
    mBottomItemArrayList.add(bottomItem);
    return this;
}

添加item之后会返会对象本身,就可以继续.addItem()了,就像这样

mAnimationBottomBar.addItem(new BottomItem(R.drawable.h, "zero"))
                    .addItem(new BottomItem(R.drawable.h, "one"))
                    .addItem(new BottomItem(R.drawable.h, "two"))
                    .addItem(new BottomItem(R.drawable.h, "four"))
                    .addItem(new BottomItem(R.drawable.h, "five"))

好了,现在已经添加了item,嗯?球都没得.运行没有显示出来,当然啦添加了之后需要添加到ViewGroup里,在经过onMeasure和onLayout之后才会显示出来

public void build()  {
    itemCount = mBottomItemArrayList.size();
    itemWidth=getLayoutParams().width/itemCount;/*获得平均一个item的宽度,这里有个问题,因为这个时候还没有经过OnMeaSure(),width获取不到,在onMeasure里可以再次进行调整*/
    for (BottomItem bottomItem : mBottomItemArrayList) {/*添加图标*/
        ImageView imageView = new ImageView(mContext);
        imageView.setImageResource(bottomItem.drawableRes);
        addView(imageView, itemWidth, 20);
    }
    for (BottomItem bottomItem : mBottomItemArrayList) {/*添加标题/
        TextView textView=new TextView(mContext);
        textView.setTextSize(textSize);
        textView.setText(bottomItem.title);
        textView.setTextColor(textColor);
        textView.setGravity(Gravity.CENTER);
        addView(textView,itemWidth,20);
    }
}

onMeasure(),遍历刚刚所有添加子View,通知它们测量自己的长宽

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    childCount = getChildCount();/*获得所有子View的数量*/
    barWidth = getSize(300,widthMeasureSpec);//bottombar的宽度
    barHeight =  getSize(300,heightMeasureSpec);//--的高度
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        childView.getLayoutParams().width=itemWidth;/*调整子view的宽度*/
    }
}

onLayout(),确定所有的子View应该在的位置

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  
    for (int i = 0; i < itemCount; i++) {/*遍历每一个item,放置item的位置*/
        itemCenterX[i] = (int) (itemWidth * (i + 0.5));/*记录每个item的中心位置*/
        View childImageView = getChildAt(i);
        childImageView.layout(itemWidth * i, 0, itemWidth * (i + 1), 100);//放置图标,
        View childTextView=getChildAt(itemCount+i);
        childTextView.layout(itemWidth * i+childTextView.getWidth()/4,100,itemWidth * (i + 1),barHeight);/*放置标题*/
    }
}

此时的样子应该是这样的

20171104150978671165751.png
20171104150978671165751.png

添加背景颜色

你可能会想到用setBackGroundColor()来设置背景颜色,不过不要忘了,我们这个是要实现动画效果的,虽然使用setBackGroundColor()也能实现,但是要复杂一些.我决定使用OnDraw()画出来,在ViewGroup里默认是不调用OnDraw()的具体原因见这里解决方法也很简单

如果我们要重写一个ViweGroup的onDraw方法,有两种方法:
1.在构造函数里面,给其设置一个颜色,如#00000000。
2.在构造函数里面,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。

我选择了第二个方法,因为我们要自己实现背景.

@Override
protected void onDraw(Canvas canvas) {
    /*绘制item颜色*/
    for (int i = 0; i < 5; i++) {
        mPaint.setColor(itemcolors[i]);
        canvas.drawRect(itemWidth * i, 0, itemWidth * (i + 1), barHeight, mPaint);
        canvas.save();
    }
        
    /*画出背景,两个长方形*/
    mPaint.setColor(backGroundColor);
    canvas.drawRect(0, 0, itemMoveLeft, barHeight, mPaint);
    canvas.drawRect(itemMoveRight, 0, itemWidth * 5, barHeight, mPaint);
    canvas.save();
    super.onDraw(canvas);
}

这里我分了两部分来画,一是每个item的背景颜色,二是整体的背景颜色,注意画的先后顺序哦,我为了实现item的移动,把item部分画在下层,把背景画在了上层,通过改变背景来实现item的移动效果.
这时候的效果是这样的

20171104150978685438558.png
20171104150978685438558.png

实现动画

注意这里的动画其实分为两个部分,两部分是同时进行的

  1. item的移动动画
  2. item的缩放动画
@Override
protected void onDraw(Canvas canvas) {
    /绘制item颜色*/
    for (int i = 0; i < 5; i++) {
        mPaint.setColor(itemcolors[i]);
        canvas.drawRect(itemWidth * i, 0, itemWidth * (i + 1), barHeight, mPaint);
        canvas.save();
    }
    /*画出背景,两个长方形*/
    mPaint.setColor(backGroundColor);
    canvas.drawRect(0, 0, itemMoveLeft, barHeight, mPaint);
    canvas.drawRect(itemMoveRight, 0, itemWidth * 5, barHeight, mPaint);
    canvas.save();
    /*遍历每个item位置,画出需要移动和缩放的item*/
    for (int i = 0; i < itemCount; i++) {
        int deltaX=Math.abs(itemMoveCenter-itemCenterX[i]);/*获得当前item移动中心点和item固定中心点的距离*/
        if (deltaX<itemWidth){
            itemScale[i]= (float) (-0.5*deltaX/itemWidth+1);/*当距离小于一个item的宽度时调整item的缩放系数*/
        }
        else itemScale[i]=0.5f;/*非选中的item的缩放系数固定为0.5*/
        
        /*对item的大小进行缩放*/
        View childImageView = getChildAt(i);
        childImageView.setScaleX(itemScale[i]);
        childImageView.setScaleY(itemScale[i]);
        View childTextView = getChildAt(itemCount+i);
        childTextView.setScaleX(itemScale[i]);
        childTextView.setScaleY(itemScale[i]);
    }
    super.onDraw(canvas);
}

我用了几个数组来记录每个item的固定中心位置,每个item的颜色,每个item的缩放系数.
缩放系数这里,默认的未选中item的缩放系数是0.5,选中的item的缩放系数就是1.0,移动的时候,越靠近选中的item就这个系数就越大.
既然是动画我们肯定要让她动起来,我继承了Animation类实现了自己的BottomAnimation类

private class BottomAnimation extends Animation {
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        super.applyTransformation(interpolatedTime, t);
        int position = selectIndex - selectLastIndex;
        /*判断不同方向的移动*/
        if (position < 0) {/*向左滑动*/
            itemMoveRight = (int) (itemMoveLastRight + interpolatedTime * itemWidth * position);
            itemMoveLeft = (int) (itemMoveLastLeft + setFirst(interpolatedTime) * itemWidth * position);
            itemMoveCenter = (int) (itemMoveLastRight + interpolatedTime * itemWidth * position) -itemWidth / 2;/*记录中心点移动的位置*/
        } else {/*向右滑动*/
            itemMoveRight = (int) (itemMoveLastRight + setFirst(interpolatedTime) * itemWidth * position);
            itemMoveLeft = (int) (itemMoveLastLeft + interpolatedTime * itemWidth * position);
            itemMoveCenter = (int) (itemMoveLastLeft + interpolatedTime * itemWidth * position) + itemWidth / 2;/*记录中心点移动的位置*/
        }
        postInvalidate();/*更新画面*/
    }
    /*为了实现果冻效果,先移动的一侧要有快速效果*/
    private float setFirst(float interpolatedTime) {
        return (float) Math.sin(interpolatedTime * 0.5 * Math.PI);
    }
}

在判断到有点击事件之后,启动这个动画就ok了

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            touchDownX = ev.getX();
            break;
        case MotionEvent.ACTION_UP:
            if (ev.getX() / itemWidth == touchDownX / itemWidth) {
                selectIndex = (int) (ev.getX() / itemWidth);
                /*点击时开始动画*/
                startAnimation(mBottomAnimation);
            }
            break;
    }
    return true;
}

最后的效果是这个样子的

20171104150978699755982.gif
20171104150978699755982.gif

最后完整的的代码在我的github