Android實現(xiàn)炫酷的上下翻動切換效果
需求描述
現(xiàn)在市面上主流的app的主界面,都是底下一排切換按鈕,上面顯示不同的界面。對于一些功能比較少的app,又想吸引人的app,咋辦呢?那當然就是功能不夠,效果來湊~
一般的RadioButton都是像這樣,圖片很文字同時顯示,選中的高亮操作:

這不夠風騷,我們來做一個圖片和文字滾動切換,效果炫酷的RadioButton:

看到這里,雖然也沒有很炫酷,但至少比單調(diào)的高亮要強那么一點吧~
功能實現(xiàn)
選擇控件
當業(yè)務(wù)丟給你這么一個效果讓你實現(xiàn),我們首先要挑選合適的控件,在這里,很顯然選用RadioGroup會有很大優(yōu)勢。因為我們只需要自定義RadioButton,點擊其他按鈕更換選中和非選中效果時,通過重寫setChecked(boolean checked)方法,就能夠輔助我們實現(xiàn)各自的操作和狀態(tài)。
效果拆解
這里涉及到幾個效果:
文字和圖片的切換
默認顯示圖片,選中時顯示文字。有個上下翻動的效果,其實這里我們只需要把圖片和文字豎著排列,用圖展示就是這樣:
當選中的時候,把整體往上移動,將文字顯示在視野中,圖片被移動到上邊界之外。
底部的小點切換
底部圓點只需要選中的時候畫出來,未選中啥都不畫就行了。遮擋層的繪制
有的朋友可能會問,什么遮擋層,這里那里有遮擋嗎,答案是有的。如果只是簡單的進行上下移動操作,是不需要用到這個的,但是仔細觀察效果好像是斜著上下切換的,這邊用了一個視覺欺騙,我把相關(guān)部分染色一下,瞬間就能看出貓膩了:

可以很清楚的看到有兩條斜杠擋在了前面,由于和底色相同,在視覺上給了我們一種斜著切換的效果。
代碼實現(xiàn)
具體的實現(xiàn)方案已經(jīng)分析完畢,剩下的就是通過代碼實現(xiàn)這個控件了~
首先在attr.xml中,定義一些可以自定義的屬性:
<declare-styleable name="FantasticRadioButton" tools:ignore="ResourceName"><!-- 圖片的引用 --><attr name="fantastic_drawable" format="reference" /><!-- 底部小點的顏色 --><attr name="bottom_dot_color" format="color" /><!-- 圖片的高度 --><attr name="icon_width" format="dimension" /><!-- 圖片的寬度 --><attr name="icon_height" format="dimension" /><!-- 底部小點的寬度 --><attr name="bottom_dot_width" format="dimension" /><!-- 文字內(nèi)容 --><attr name="label" format="string" /><!-- 文字大小 --><attr name="label_size" format="dimension" /><!-- 文字顏色 --><attr name="label_color" format="color" /><!-- 遮罩的顏色,設(shè)置成和底色一致 --><attr name="bg_color" format="color" /></declare-styleable>
繼承AppCompatRadioButton進行自定義,獲取一些在xml中定義的屬性:
public FantasticRadioButton(Context context, AttributeSet attrs) {super(context, attrs);initialize(context, attrs, 0);}private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FantasticRadioButton);mBgColor = a.getColor(R.styleable.FantasticRadioButton_bg_color, Color.WHITE);mBottomDotColor = a.getColor(R.styleable.FantasticRadioButton_bottom_dot_color, Color.parseColor("#EE82EE"));mBottomStaticRectWidth = a.getDimensionPixelOffset(R.styleable.FantasticRadioButton_bottom_dot_width, SysUtils.convertDpToPixel(4));mDrawableWidth = a.getDimensionPixelOffset(R.styleable.FantasticRadioButton_icon_width, 0);mDrawableHeight = a.getDimensionPixelOffset(R.styleable.FantasticRadioButton_icon_height, 0);mTextStr = SysUtils.getSafeString(a.getString(R.styleable.FantasticRadioButton_label));mTextColor = a.getColor(R.styleable.FantasticRadioButton_label_color, Color.GREEN);mTextSize = a.getDimensionPixelOffset(R.styleable.FantasticRadioButton_label_size, SysUtils.convertDpToPixel(16));Drawable drawable = a.getDrawable(R.styleable.FantasticRadioButton_fantastic_drawable);if (drawable != null) {mIconBitmap = drawableToBitmap(drawable);}a.recycle();mTargetDistances = mDrawableHeight * 3f;setButtonDrawable(null);mPaint.setAntiAlias(true);mPaint.setStrokeWidth(5f);mCirclePaint.setAntiAlias(true);mCirclePaint.setColor(mBottomDotColor);mCirclePaint.setStrokeWidth(5f);mCirclePaint.setStyle(Paint.Style.FILL);}
這里的mTargetDistances是指的圖文需要移動的總距離,這里設(shè)置為圖片高度的三倍。mCirclePaint是專門繪制底部小圓點的畫筆。
設(shè)置寬高
@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);if (w > 0 && h > 0) {mWidth = w - mInnerPaddingX * 2;mHeight = h - mInnerPaddingY * 2;// 確定畫布的中心點mCenterX = w / 2;mCenterY = h / 2;}}
在onSizeChanged回調(diào)中,這時候視圖的寬高已經(jīng)確定,因為在這里我們給寬高和中心點的x和y進行賦值。
繪制圖片和文字
private void drawIconAndText(Canvas canvas) {canvas.save();int left = mCenterX - mIconBitmap.getWidth() / 2;int top = mCenterY - mIconBitmap.getHeight() / 2;canvas.drawBitmap(mIconBitmap, left, top - mTransDistances, mPaint);// 畫字float scaledSizeInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,mTextSize, getResources().getDisplayMetrics());mTextPaint.setAntiAlias(true);mTextPaint.setStyle(Paint.Style.FILL);mTextPaint.setTextSize(scaledSizeInPixels);mTextPaint.setTextAlign(Paint.Align.CENTER);mTextPaint.setFakeBoldText(true);mTextPaint.setColor(mTextColor);Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();float baseline = (mHeight + mTargetDistances - mTransDistances - fontMetrics.bottom - fontMetrics.top) / 2;canvas.drawText(mTextStr, mCenterX, baseline, mTextPaint);canvas.restore();}
就是很基礎(chǔ)的繪制一個bitmap和text,需要注意的是,這里有一個變量:mTransDistances,這是一個很關(guān)鍵的變量,他代表的是當前移動的距離,我們就通過改變這個變量,來實現(xiàn)移動的效果!
繪制底部小點
private void drawBottomDot(Canvas canvas) {canvas.save();canvas.drawRoundRect(new RectF(mCenterX - mBottomRectWidth / 2, mHeight - mBottomRectHeight - ONE_DP, mCenterX + mBottomRectWidth / 2, mHeight - ONE_DP),mBottomRectHeight / 2, mBottomRectHeight / 2, mCirclePaint);canvas.restore();}
這里的mButtomRectWidth和mBottomHeight也是變量,通過改變這兩個值,來實現(xiàn)縮放的效果。
繪制遮罩層
private void drawLayerPath(Canvas canvas) {canvas.save();float upTransDistance = -mLayerHeight / 2;Path path = new Path();path.moveTo(mCenterX - mWidth / 3, upTransDistance);path.lineTo(mCenterX + mWidth / 3, upTransDistance + SysUtils.convertDpToPixel(10));path.lineTo(mCenterX + mWidth / 3, upTransDistance + SysUtils.convertDpToPixel(10) + mLayerHeight);path.lineTo(mCenterX - mWidth / 3, upTransDistance + mLayerHeight);path.lineTo(mCenterX - mWidth / 3, upTransDistance);path.close();mPaint.setColor(mBgColor);mPaint.setStyle(Paint.Style.FILL);canvas.drawPath(path, mPaint);Path path2 = new Path();float startY = upTransDistance + mLayerHeight * 2;path2.moveTo(mCenterX - mWidth / 3, startY);path2.lineTo(mCenterX + mWidth / 3, startY + SysUtils.convertDpToPixel(10));path2.lineTo(mCenterX + mWidth / 3, startY + SysUtils.convertDpToPixel(10) + mLayerHeight);path2.lineTo(mCenterX - mWidth / 3, startY + mLayerHeight);path2.lineTo(mCenterX - mWidth / 3, startY);path2.close();canvas.drawPath(path2, mPaint);canvas.restore();}
這里沒啥特別的,就是繪制兩條斜杠在畫布上。
過渡動畫制作
這里采用屬性動畫,通過改變上面的變量,然后postInvalidate調(diào)用onDraw()來重繪界面實現(xiàn)過渡效果:
private ValueAnimator startBottomLineAnimation() {ValueAnimator lineAnimation = ValueAnimator.ofFloat(0f, mBottomStaticRectWidth);lineAnimation.setDuration(DURATION_TIME);lineAnimation.setInterpolator(new TimeInterpolator() {@Overridepublic float getInterpolation(float v) {return 1 - (1 - v) * (1 - v);}});lineAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mBottomRectWidth = (float) animation.getAnimatedValue();// 高度是寬度的五分之一mBottomRectHeight = mBottomRectWidth / 5;postInvalidate();}});return lineAnimation;}
這是底部小圓點的動畫,重寫插播器來控制動畫的速度,先快后慢,關(guān)于動畫插播器的知識,百度有很多資料~這里通過屬性動畫控制小圓點的高度和寬度。
private ValueAnimator getIconAndTextAnimation() {ValueAnimator transAnimation = ValueAnimator.ofFloat(0f, mTargetDistances);transAnimation.setDuration(DURATION_TIME);transAnimation.setInterpolator(new TimeInterpolator() {@Overridepublic float getInterpolation(float v) {return 1 - (1 - v);}});transAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mTransDistances = (float) animation.getAnimatedValue();postInvalidate();}});transAnimation.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationCancel(Animator animation) {mIsTransEnd = true;postInvalidate();}@Overridepublic void onAnimationEnd(Animator animation) {mIsTransEnd = true;postInvalidate();}@Overridepublic void onAnimationStart(Animator animation) {mIsTransEnd = false;postInvalidate();}@Overridepublic void onAnimationPause(Animator animation) {mIsTransEnd = true;postInvalidate();}});return transAnimation;}
這里是改變圖文平移距離的屬性動畫控制器,并對動畫狀態(tài)進行監(jiān)聽,只有在動畫執(zhí)行過程中,也就是切換的時候,才繪制遮擋層。
繪制
準備工作都做完了,只差最后一步繪制了!
protected void onDraw(Canvas canvas) {super.onDraw(canvas);if (mWidth <= 0 || mHeight <= 0 || mIconBitmap == null || mIconBitmap.isRecycled()) {return;}// 先畫圖片和文字切換的部分if (mIconBitmap != null) {drawIconAndText(canvas);}// 畫一個遮擋層,為了遮住放在圖片下面的文字float layerHeight = (getHeight() - mIconBitmap.getHeight()) / 3;mPaint.setColor(mBgColor);canvas.drawRect(0, getHeight() - layerHeight, mWidth, getHeight(), mPaint);// 最后畫遮擋條if (!mIsTransEnd) {drawLayerPath(canvas);}// 最后最后畫底部圓圈drawBottomDot(canvas);}
繪制的時候只需要注意繪制順序就行了,因為他是一層層往上繪制的。
最后一步
至此,一個自定義RadioButton算是完成了,最后只需要重寫setChecked(boolean checked)函數(shù),根據(jù)是否選中來執(zhí)行對應(yīng)的動畫,就大功告成了:
@Overridepublic void setChecked(boolean checked) {boolean isChanged = checked != isChecked();super.setChecked(checked);// ValueAnimator bottomCircleAni = getCircleAnimation();ValueAnimator bottomLineAni = startBottomLineAnimation();ValueAnimator iconAni = getIconAndTextAnimation();if (isChanged) {if (checked) {// startCircleAnimator();bottomLineAni.start();iconAni.start();postInvalidate();} else {bottomLineAni.reverse();iconAni.reverse();postInvalidate();}}}
使用
使用方法很簡單,直接用原生RadioGroup包裹我們自定義的RadioButton就行了,然后把RadioButton設(shè)置一下屬性即可:

總結(jié)
自定義樣式的RadioButton多種多樣,這里也是給大家提供一種樣式,當然,重要的是解決問的思路和方法,辦法總比困難多,不斷試錯,找到正確的道路,那么距離解決問題也不會遠了!
源碼地址:
https://github.com/wx9265661/SmallDemos2
到這里就結(jié)束啦。
