标签:
现在新版360手机助手的界面都做得挺漂亮的,在切换底部导航时的波纹效果也很好看,刚好最近看了个开源项目才了解到原来Drawable做动画效果也怎么好用,所以就仿照360实现了下带波纹的TabHost。源代码地址:https://github.com/Rukey7/XFragmentTabHost
先来看一下实现后的效果:
说明一下实现要点:
1. 因为我们项目之前用的是FragmentTabHost,所以我直接继承FragmentTabHost来实现动画效果更方便;
2. 波纹动画的实现其实是自定义带动画效果的Drawable,然后将Drawable设置为Tab菜单的背景;
3. 其它的就是一些Tab菜单切换的处理了。
一. 自定义波纹Drawable
自定义Drawable只要继承Drawable并实现以下4个方法,同时实现Animatable接口:
public class RippleDrawable extends Drawable implements Animatable { @Override public void draw(Canvas canvas) { // 绘图 } @Override public void setAlpha(int alpha) { // 设置透明度 } @Override public void setColorFilter(ColorFilter colorFilter) { // 设置颜色过滤 } @Override public int getOpacity() { // 设置颜色格式 return PixelFormat.RGBA_8888; } @Override public void start() { // 启动动画 } @Override public void stop() { // 停止动画 } @Override public boolean isRunning() { // 判断动画是否运行 return false; } }这几个方法中最重要的就是draw()方法了,相信自定义过View的都知道我们图形就是在这里绘制,这里也一样,其它方法在这里影响不大,最后一个方法用来设置Drawable的颜色格式。要实现动画Drawable需要实现Animatable接口,并实现3个方法如下,其实不实现这个接口也能做动画效果,但还是实现比较好。
下面是整个波纹Drawable的实现代码:
/** * Created by long on 2016/6/27. * 波纹Drawable */ public class RippleDrawable extends Drawable implements Animatable { /** * 3种模式:左边、中间和右边波纹 */ public static final int MODE_LEFT = 1; public static final int MODE_MIDDLE = 2; public static final int MODE_RIGHT = 3; private int mMode = MODE_MIDDLE; // 前景色和后景色画笔 private Paint mPaintFront; private Paint mPaintBehind; // 用来绘制扇形的矩形框 private RectF mRect; // 目标View的宽高的一半 private int mHalfWidth; private int mHalfHeight; // 扩散半径 private int mRadius; // 前景色和背景色的分割距离 private int mDivideSpace; // 扩散满视图需要的距离,中点到斜角的距离 private int mFullSpace; // 动画控制 private ValueAnimator mValueAnimator; public RippleDrawable(int frontColor, int behindColor, int mode) { mPaintFront = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintFront.setColor(frontColor); mPaintBehind = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBehind.setColor(behindColor); mRect = new RectF(); mMode = mode; } @Override public void draw(Canvas canvas) { if (mRadius > mHalfWidth) { int count = canvas.save(); canvas.drawCircle(mHalfWidth, mHalfHeight, mHalfWidth, mPaintBehind); canvas.restoreToCount(count); count = canvas.save(); canvas.drawCircle(mHalfWidth, mHalfHeight, mDivideSpace, mPaintFront); canvas.restoreToCount(count); } else if (mRadius > mDivideSpace) { int count = canvas.save(); canvas.drawCircle(mHalfWidth, mHalfHeight, mRadius, mPaintBehind); canvas.restoreToCount(count); count = canvas.save(); canvas.drawCircle(mHalfWidth, mHalfHeight, mDivideSpace, mPaintFront); canvas.restoreToCount(count); } else { canvas.drawCircle(mHalfWidth, mHalfHeight, mRadius, mPaintFront); } // 左右两边才进行扇形绘制 if (mMode != MODE_MIDDLE) { mRect.left = mHalfWidth - mRadius; mRect.right = mHalfWidth + mRadius; mRect.top = mHalfHeight - mRadius; mRect.bottom = mHalfHeight + mRadius; } if (mMode == MODE_LEFT) { canvas.drawArc(mRect, 90, 180, true, mPaintFront); } else if (mMode == MODE_RIGHT) { canvas.drawArc(mRect, -90, 180, true, mPaintFront); } } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public int getOpacity() { return PixelFormat.RGBA_8888; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); mHalfHeight = (bounds.bottom - bounds.top) / 2; mHalfWidth = (bounds.right - bounds.left) / 2; mDivideSpace = Math.max(mHalfHeight, mHalfWidth) * 3 / 4; mFullSpace = (int) Math.sqrt(mHalfWidth * mHalfWidth + mHalfHeight * mHalfHeight); // 属性动画 mValueAnimator = ValueAnimator.ofInt(0, mFullSpace); mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mRadius = (int) animation.getAnimatedValue(); invalidateSelf(); } }); mValueAnimator.setDuration(200); start(); } @Override public void start() { mValueAnimator.start(); } @Override public void stop() { mValueAnimator.end(); } @Override public boolean isRunning() { return mValueAnimator != null && mValueAnimator.isRunning(); } }整体还是比较简单的,主要就是绘图那里需要绘制3个图形,一个前景色的圆形、一个后景色的圆形和左右两边的扇形。在绘制前需要计算前景色和后景色绘制的半径,中点都为Tab视图的中心。这里需要实现onBoundsChange(Rect bounds)方法,在这里可以获取到Tab菜单项的尺寸信息,这里的mDivideSpace是前景色圆形的半径,也就是前景和后景的分割距离,而后景色圆形半径为Tab项宽度的一半。最后就剩下左右两边需要填充Tab边角的扇形半径mFullSpace了,距离就是中心到Tab边角点的距离了。
当然了,要实现动画效果肯定不止这些,还有一个重要的ValueAnimator,通过它来控制波纹的扩散半径,用法还是很简单的,用过属性动画的应该都不陌生。这里面需要注意的是里面调用了一个方法invalidateSelf() ,Drawable是通过这个方法来进行重绘的,它会重新调用draw()方法来实现波纹效果。
二. 实现扩展的FragmentTabHost
要实现扩展的FragmentTabHost需要继承它并实现一个重要的方法setCurrentTab(int index),当FragmentTabHost在选择Tab菜单时会调用该方法,在这方法里我们可以得到当前选中的项和之前选中的项,并做动画处理。
在实现FragmentTabHost之前,我们的Tab菜单布局生成也通过这里实现,并提供方法让外面调用,首先是菜单布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="@dimen/tab_height" android:minWidth="@dimen/tab_min_width" android:paddingTop="@dimen/tab_padding_top_inactive" android:paddingBottom="@dimen/tab_padding_bottom" android:background="?selectableItemBackgroundBorderless"> <ImageView android:id="@+id/tab_icon" android:layout_width="@dimen/tab_icon" android:layout_height="@dimen/tab_icon" android:layout_gravity="center_horizontal"/> <TextView android:id="@+id/tab_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:lines="1" android:text="首页" android:textColor="@color/colorInactive" android:textSize="@dimen/tab_text_size_inactive"/> </LinearLayout>这个很简单,就是图标和标题,和正常使用没区别。然后是Tab菜单类:
/** * Created by long on 2016/4/15. * Tab项 */ public class TabItem { private String title; private int imageRes; public TabItem(String title, int imageRes) { this.title = title; this.imageRes = imageRes; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public int getImageRes() { return imageRes; } public void setImageRes(int imageRes) { this.imageRes = imageRes; } }同样很简单,和布局文件对应一个图标和一个标题。
最后看下扩展FragmentTabHost的实现:
/** * Created by long on 2016/4/15. * 扩展TabHost */ public class XFragmentTabHost extends FragmentTabHost { private Context mContext; private List<View> mTabViews; private List<TabItem> mTabItems; // 字体激活颜色 private int mTextActiveColor; private int mTextInactiveColor; // 字体激活大小 private float mTextActiveSize; private float mTextInactiveSize; // 视图激活对顶部的偏移 private int mViewActivePaddingTop; private int mViewInactivePaddingTop; // 波纹模式的前景颜色和后景颜色 private int mFrontColor; private int mBehindColor; // TabHost模式 private TabMode mTabMode; public XFragmentTabHost(Context context) { super(context); _init(context); } public XFragmentTabHost(Context context, AttributeSet attrs) { super(context, attrs); _init(context); } private void _init(Context context) { mTabViews = new ArrayList<>(); mTabItems = new ArrayList<>(); mContext = context; mTextActiveColor = ContextCompat.getColor(mContext, R.color.colorActive); mTextInactiveColor = ContextCompat.getColor(mContext, R.color.colorInactive); mFrontColor = ContextCompat.getColor(mContext, R.color.colorFront); mBehindColor = ContextCompat.getColor(mContext, R.color.colorBehind); mTextActiveSize = getResources().getDimension(R.dimen.tab_text_size_active); mTextInactiveSize = getResources().getDimension(R.dimen.tab_text_size_inactive); mViewActivePaddingTop = (int) getResources().getDimension(R.dimen.tab_padding_top_active); mViewInactivePaddingTop = (int) getResources().getDimension(R.dimen.tab_padding_top_inactive); mTabMode = TabMode.MoveToTop; } /** * 覆写父类接口,并在这里做些动画特效 * @param index 当前选中的Tab项 */ @Override public void setCurrentTab(int index) { // 获取之前选中的index int lastIndex = getCurrentTab(); super.setCurrentTab(index); // 选中不同的Tab项才做切换处理 if (lastIndex != index) { _switchTab(lastIndex, index); } } /** * 添加TabItem * @param item TabItem * @param fragClass fragment类名 * @param bundle 传给fragment的参数 */ public void addTabItem(TabItem item, Class<?> fragClass, Bundle bundle) { mTabItems.add(item); View view = _getIndicator(item); mTabViews.add(view); this.addTab(newTabSpec(item.getTitle()).setIndicator(view), fragClass, bundle); } /** * 获取TabItem视图 * @param item TabItem * @return */ private View _getIndicator(TabItem item) { View view = LayoutInflater.from(mContext).inflate(R.layout.tab_indicator, null); ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon); TextView title = (TextView) view.findViewById(R.id.tab_title); imageView.setImageResource(item.getImageRes()); title.setText(item.getTitle()); title.setTextColor(mTextInactiveColor); return view; } /** * 切换Tab * @param lastIndex 上一个选中索引 * @param nextIndex 下一个选中索引 */ private void _switchTab(int lastIndex, int nextIndex) { for (int i = 0; i < mTabViews.size(); i++) { if (i == lastIndex) { _doRipple(i, false); } else if (i == nextIndex) { _doRipple(i, true); } } } /** * 波纹处理 * @param index 索引 * @param isActivated 是否激活 */ private void _doRipple(int index, boolean isActivated) { View view = mTabViews.get(index); View tabView = view.findViewById(R.id.tab_layout); TextView title = (TextView) view.findViewById(R.id.tab_title); if (index == 0) { _rippleDrawable(tabView, mFrontColor, mBehindColor, RippleDrawable.MODE_LEFT, isActivated); } else if (index == (mTabViews.size() - 1)){ _rippleDrawable(tabView, mFrontColor, mBehindColor, RippleDrawable.MODE_RIGHT, isActivated); } else { _rippleDrawable(tabView, mFrontColor, mBehindColor, RippleDrawable.MODE_MIDDLE, isActivated); } if (isActivated) { title.setTextColor(mTextActiveColor); } else { title.setTextColor(mTextInactiveColor); } } /** * 波纹动画 * @param view * @param frontColor * @param behindColor * @param mode * @param isActivated */ @SuppressWarnings("deprecation") private void _rippleDrawable(final View view, int frontColor, int behindColor, int mode, boolean isActivated) { if (isActivated) { RippleDrawable rippleDrawable = new RippleDrawable(frontColor, behindColor, mode); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view.setBackground(rippleDrawable); } else { view.setBackgroundDrawable(rippleDrawable); } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view.setBackground(null); } else { view.setBackgroundDrawable(null); } } } /** * 属性设置 * @return */ public int getTextActiveColor() { return mTextActiveColor; } public void setTextActiveColor(int textActiveColor) { mTextActiveColor = textActiveColor; } public int getTextInactiveColor() { return mTextInactiveColor; } public void setTextInactiveColor(int textInactiveColor) { mTextInactiveColor = textInactiveColor; } public int getFrontColor() { return mFrontColor; } public void setFrontColor(int frontColor) { mFrontColor = frontColor; } public int getBehindColor() { return mBehindColor; } public void setBehindColor(int behindColor) { mBehindColor = behindColor; } }其实也不会复杂,就是在切换Tab菜单时,对选中菜单设置背景为RippleDrawable,对之前的菜单背景设置为空,就这么简单^ ^,使用的话大体和FragmentHost是基本一样的,就添加Tab菜单使用上面实现的方法addTabItem(TabItem item, Class<?> fragClass, Bundle bundle) 就行了,具体下载源代码查看。
这个TabHost实现还是不复杂,处理波纹效果外,源代码里还有一些其它动画效果,实现思路都一样,有兴趣也可以自己定制些更好看的动画效果~
标签:
原文地址:http://blog.csdn.net/github_35180164/article/details/51771842