[置頂] 實現360手機助手TabHost的波紋效果
來源:程序員人生 發布時間:2016-07-15 09:38:02 閱讀次數:2525次
現在新版360手機助手的界面都做得挺漂亮的,在切換底部導航時的波紋效果也很好看,恰好最近看了個開源項目才了解到原來Drawable做動畫效果也怎樣好用,所以就仿照360實現了下帶波紋的TabHost。源代碼地址:https://github.com/Rukey7/XFragmentTabHost
先來看1下實現后的效果:

說明1下實現要點:
1. 由于我們項目之前用的是FragmentTabHost,所以我直接繼承FragmentTabHost來實現動畫效果更方便;
2. 波紋動畫的實現實際上是自定義帶動畫效果的Drawable,然后將Drawable設置為Tab菜單的背景;
3. 其它的就是1些Tab菜單切換的處理了。
1. 自定義波紋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的都知道我們圖形就是在這里繪制,這里也1樣,其它方法在這里影響不大,最后1個方法用來設置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的寬高的1半
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, ⑼0, 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個圖形,1個前風景的圓形、1個后風景的圓形和左右兩邊的扇形。在繪制前需要計算前風景和后風景繪制的半徑,中點都為Tab視圖的中心。這里需要實現
onBoundsChange(Rect bounds)方法,在這里可以獲得到Tab菜單項的尺寸信息,這里的
mDivideSpace是前風景圓形的半徑,也就是前景和后景的分割距離,而后風景圓形半徑為Tab項寬度的1半。最后就剩下左右兩邊需要填充Tab邊角的扇形半徑
mFullSpace了,距離就是中心到Tab邊角點的距離了。
固然了,要實現動畫效果肯定不止這些,還有1個重要的ValueAnimator,通過它來控制波紋的分散半徑,用法還是很簡單的,用過屬性動畫的應當都不陌生。這里面需要注意的是里面調用了1個方法invalidateSelf() ,Drawable是通過這個方法來進行重繪的,它會重新調用draw()方法來實現波紋效果。
2. 實現擴大的FragmentTabHost
要實現擴大的FragmentTabHost需要繼承它并實現1個重要的方法setCurrentTab(int index),當FragmentTabHost在選擇Tab菜單時會調用該方法,在這方法里我們可以得到當前選中的項和之前選中的項,并做動畫處理。
在實現FragmentTabHost之前,我們的Tab菜單布局生成也通過這里實現,并提供方法讓外面調用,首先是菜單布局:
<?xml version="1.0" encoding="utf⑻"?>
<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;
}
}
一樣很簡單,和布局文件對應1個圖標和1個標題。
最后看下擴大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 上1個選中索引
* @param nextIndex 下1個選中索引
*/
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是基本1樣的,就添加Tab菜單使用上面實現的方法
addTabItem(TabItem item, Class<?> fragClass, Bundle bundle) 就好了,具體下載源代碼查看。
這個TabHost實現還是不復雜,處理波紋效果外,源代碼里還有1些其它動畫效果,實現思路都1樣,有興趣也能夠自己定制些更好看的動畫效果~
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈