接著上一篇博客http://blog.csdn.net/sdfdzx/article/details/75447981,由于需求變動(dòng),需要星星在滑動(dòng)的時(shí)候能夠有動(dòng)畫(huà)效果,由于CustomRatingBar是基于自定義View,實(shí)現(xiàn)onDraw繪制而成,實(shí)現(xiàn)動(dòng)畫(huà)效果比較困難,所以只能考慮從用另一個(gè)方式實(shí)現(xiàn)這個(gè)組件,這篇博文就是用ViewGroup實(shí)現(xiàn)自定義評(píng)分條并且實(shí)現(xiàn)動(dòng)畫(huà)效果。
功能特性
效果
1.可設(shè)置星星大小
2.可設(shè)置星星之間的間距
3.可以設(shè)置星星圖片(填充圖片和半填充圖片)
4.可以設(shè)置星星是否可觸摸評(píng)分
5.可設(shè)置評(píng)分范圍(整顆 | 半顆 ) 此處不支持隨意
6.可以設(shè)置總星量
實(shí)現(xiàn)思路
1.利用自定義ViewGroup繼承LinearLayout,動(dòng)態(tài)添加ImageView實(shí)現(xiàn)。
2.根據(jù)進(jìn)度動(dòng)態(tài)設(shè)置ImageView的圖片背景。
3.重寫(xiě)onTouchEvent,利用屬性動(dòng)畫(huà)在MOVE事件時(shí)實(shí)現(xiàn)動(dòng)畫(huà)效果。
實(shí)現(xiàn)難點(diǎn)
1.觸摸進(jìn)度的判斷。
2.屬性動(dòng)畫(huà)的實(shí)現(xiàn)。
3.一定的滑動(dòng)沖突處理。
整體代碼
package com.study.dzx.library.widget;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.study.dzx.library.R;
import com.study.dzx.library.utils.DensityUtils;
/**
* Author : Xuan.
* Data : 2017/7/12.
* Description :
* 星星評(píng)分-viewgroup
* -可動(dòng)畫(huà)
*/
public class CustomAnimRatingBar extends LinearLayout {
//星星個(gè)數(shù)
private int mStarNum;
//星星之間的距離
private int mStarDistance;
//星星的大小
private int mStarSize;
//空星星圖片
private Drawable mEmptyStar;
//填充的星星的照片
private Drawable mFillStar;
//半個(gè)星星的圖片
private Drawable mHalfStar;
//星星的進(jìn)度
private float mTouchStarMark;
//上次的星星進(jìn)度
private float mLastMark;
//是否顯示半個(gè)
private boolean mShowHalf;
//顯示星星的個(gè)數(shù)
private int mShowNum;
//觸摸模式 1--單個(gè)星星 2--半個(gè)星星
private int mMode;
//是否可以觸摸
private boolean mTouchAble;
private int mLastX;
private int mLastY;
//星星變化接口
public interface onStarChangedListener {
void onStarChange(CustomAnimRatingBar ratingBar, float mark);
}
private onStarChangedListener mOnStarChangeListener;
public void setmOnStarChangeListener(onStarChangedListener mOnStarChangeListener) {
this.mOnStarChangeListener = mOnStarChangeListener;
}
private Context mContext;
public CustomAnimRatingBar(Context context) {
this(context, null);
}
public CustomAnimRatingBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomAnimRatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(HORIZONTAL);
initAttr(context, attrs);
initView();
}
/**
* 和ScrollView嵌套時(shí)滑動(dòng)沖突
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:{
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(true);
}else {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mTouchAble) {
return super.onTouchEvent(event);
}
float x = event.getX();
float touchStar = x / getWidth() * mStarNum;
if (touchStar <= 0.5f) {
touchStar = 0.5f;
}
if (touchStar > mStarNum) {
touchStar = mStarNum;
}
switch (mMode) {
case 1://整個(gè)星星
{
touchStar = (float) Math.ceil(touchStar);
break;
}
case 2://half
{
if ((touchStar - Math.floor(touchStar) <= 0.5)&& touchStar - Math.floor(touchStar)!=0) {
touchStar = (float) (Math.floor(touchStar) + 0.5f);
} else {
touchStar = (float) Math.ceil(touchStar);
}
break;
}
}
mShowHalf = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
setRating(touchStar);
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
break;
}
case MotionEvent.ACTION_MOVE: {
setRating(touchStar);
if (mTouchStarMark != mLastMark) {
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
}
mLastMark = mTouchStarMark;
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return true;
}
private void fillStar() {
mShowNum = (int) Math.ceil(mTouchStarMark);
if ((mTouchStarMark - Math.floor(mTouchStarMark)) <= 0.5f &&
mTouchStarMark - Math.floor(mTouchStarMark) != 0) {
mShowNum = (int) Math.ceil(mTouchStarMark);
mShowHalf = true;
}
for (int i = 0; i < mShowNum - 1; i++) {
((ImageView) getChildAt(i)).setImageDrawable(mFillStar);
}
if (mShowHalf) {
((ImageView) getChildAt(mShowNum - 1)).setImageDrawable(mHalfStar);
} else {
((ImageView) getChildAt(mShowNum - 1)).setImageDrawable(mFillStar);
}
resetView();
}
/**
* 設(shè)置評(píng)分
*/
public void setRating(float touchStar) {
if (mOnStarChangeListener != null) {
this.mOnStarChangeListener.onStarChange(this, touchStar);
}
mTouchStarMark = touchStar;
fillStar();
}
/**
* 獲得評(píng)分
*/
public float getRating() {
return mTouchStarMark;
}
/**
* 設(shè)置是否可以點(diǎn)擊
*/
public void setTouchAble(boolean mTouchAble) {
this.mTouchAble = mTouchAble;
}
/**
* 重置空白星星
*/
private void resetView() {
for (int i = mStarNum - 1; i > mShowNum - 1; i--) {
((ImageView) getChildAt(i)).setImageDrawable(mEmptyStar);
}
}
private void initView() {
for (int i = 0; i < mStarNum; i++) {
ImageView iv = new ImageView(mContext);
LayoutParams layoutParams = new LayoutParams(mStarSize
, mStarSize);
layoutParams.gravity = Gravity.CENTER_VERTICAL;
layoutParams.setMargins(mStarDistance / 2, 0, mStarDistance / 2, 0);
iv.setLayoutParams(layoutParams);
iv.setImageDrawable(mEmptyStar);
this.addView(iv);
}
}
private void initAttr(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomAnimRatingBar);
mStarNum = array.getInteger(R.styleable.CustomAnimRatingBar_starAnimNum, 5);
mStarDistance = array.getDimensionPixelSize(R.styleable.CustomAnimRatingBar_starAnimDistance, DensityUtils.dp2px(context, 0));
mStarSize = array.getDimensionPixelSize(R.styleable.CustomAnimRatingBar_starAnimSize, DensityUtils.dp2px(context, 20));
mEmptyStar = array.getDrawable(R.styleable.CustomAnimRatingBar_starAnimEmpty);
mFillStar = array.getDrawable(R.styleable.CustomAnimRatingBar_starAnimFill);
mHalfStar = array.getDrawable(R.styleable.CustomAnimRatingBar_starAnimHalf);
mMode = array.getInt(R.styleable.CustomAnimRatingBar_modeAnim, 2);
mTouchAble = array.getBoolean(R.styleable.CustomAnimRatingBar_touchAbleAnim, true);
mTouchStarMark = array.getInt(R.styleable.CustomAnimRatingBar_ratingAnimProgress, 0);
array.recycle();
this.mContext = context;
if (mMode == 1) {
mHalfStar = mFillStar;
}
}
}
關(guān)鍵代碼
1.initView方法
private void initView() {
for (int i = 0; i < mStarNum; i++) {
ImageView iv = new ImageView(mContext);
LayoutParams layoutParams = new LayoutParams(mStarSize
, mStarSize);
layoutParams.gravity = Gravity.CENTER_VERTICAL;
layoutParams.setMargins(mStarDistance / 2, 0, mStarDistance / 2, 0);
iv.setLayoutParams(layoutParams);
iv.setImageDrawable(mEmptyStar);
this.addView(iv);
}
}
根據(jù)對(duì)應(yīng)的星星數(shù)量,動(dòng)態(tài)添加ImageView作為為灰的星星,并且利用LayoutParams來(lái)設(shè)置星星的大小和星星之間的間距。
2.onTouchEvent()
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mTouchAble) {
return super.onTouchEvent(event);
}
float x = event.getX();
float touchStar = x / getWidth() * mStarNum;
if (touchStar <= 0.5f) {
touchStar = 0.5f;
}
if (touchStar > mStarNum) {
touchStar = mStarNum;
}
switch (mMode) {
case 1://整個(gè)星星
{
touchStar = (float) Math.ceil(touchStar);
break;
}
case 2://half
{
if ((touchStar - Math.floor(touchStar) <= 0.5)&& touchStar - Math.floor(touchStar)!=0) {
touchStar = (float) (Math.floor(touchStar) + 0.5f);
} else {
touchStar = (float) Math.ceil(touchStar);
}
break;
}
}
mShowHalf = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
setRating(touchStar);
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
break;
}
case MotionEvent.ACTION_MOVE: {
setRating(touchStar);
if (mTouchStarMark != mLastMark) {
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
}
mLastMark = mTouchStarMark;
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return true;
}
1)首先得到觸摸的坐標(biāo),利用觸摸的坐標(biāo)x/組件的長(zhǎng)度 * 星星的總個(gè)數(shù)得到需要的星星進(jìn)度。
float touchStar = x / getWidth() * mStarNum;
2)區(qū)分整個(gè)星星還是半顆星星
switch (mMode) {
case 1://整個(gè)星星
{
touchStar = (float) Math.ceil(touchStar);
break;
}
case 2://half
{
if ((touchStar - Math.floor(touchStar) <= 0.5)&& touchStar - Math.floor(touchStar)!=0) {
touchStar = (float) (Math.floor(touchStar) + 0.5f);
} else {
touchStar = (float) Math.ceil(touchStar);
}
break;
}
}
如果是整顆星星模式,則將進(jìn)度直接向上取整。
如果是半顆星星,對(duì)于小數(shù)位大于0.5的就向上取整,對(duì)于小數(shù)位小于0.5的就將整數(shù)位向下取整,再加0.5的進(jìn)度。
3)
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
setRating(touchStar);
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
break;
}
case MotionEvent.ACTION_MOVE: {
setRating(touchStar);
if (mTouchStarMark != mLastMark) {
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
}
mLastMark = mTouchStarMark;
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
首先看setRating方法
/**
* 設(shè)置評(píng)分
*/
public void setRating(float touchStar) {
if (mOnStarChangeListener != null) {
this.mOnStarChangeListener.onStarChange(this, touchStar);
}
mTouchStarMark = touchStar;
fillStar();
}
這里面進(jìn)行接口回調(diào),并且進(jìn)行星星圖片的填充。對(duì)應(yīng)fillStar()方法
private void fillStar() {
mShowNum = (int) Math.ceil(mTouchStarMark);
if ((mTouchStarMark - Math.floor(mTouchStarMark)) <= 0.5f &&
mTouchStarMark - Math.floor(mTouchStarMark) != 0) {
mShowNum = (int) Math.ceil(mTouchStarMark);
mShowHalf = true;
}
for (int i = 0; i < mShowNum - 1; i++) {
((ImageView) getChildAt(i)).setImageDrawable(mFillStar);
}
if (mShowHalf) {
((ImageView) getChildAt(mShowNum - 1)).setImageDrawable(mHalfStar);
} else {
((ImageView) getChildAt(mShowNum - 1)).setImageDrawable(mFillStar);
}
resetView();
}
這里的處理方法同CustomRatingBar,例如一個(gè)進(jìn)度是3.5,則先填充3,最后填充0.5個(gè)星星。如果是4,則先填充3,最后填充1個(gè)星星。
/**
* 重置空白星星
*/
private void resetView() {
for (int i = mStarNum - 1; i > mShowNum - 1; i--) {
((ImageView) getChildAt(i)).setImageDrawable(mEmptyStar);
}
}
resetView則將剩余的空白星星制空。
ObjectAnimator
.ofFloat(getChildAt(mShowNum - 1), "translationY", 0, -20, 0)
.setDuration(300).start();
setRating方法執(zhí)行完,則執(zhí)行動(dòng)畫(huà)效果,這里利用屬性動(dòng)畫(huà)里面的translationY,進(jìn)行跳動(dòng)效果,具體的動(dòng)畫(huà)效果也可以在此處進(jìn)行相應(yīng)的修改。
/**
* 和ScrollView嵌套時(shí)滑動(dòng)沖突
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:{
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(true);
}else {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
這里涉及到一個(gè)滑動(dòng)沖突問(wèn)題,就是當(dāng)該組件和ScrollView嵌套時(shí),向下的ScrollView滑動(dòng)和橫向的星星滑動(dòng),當(dāng)歇著滑動(dòng)的時(shí)候,就會(huì)出現(xiàn)星星的滑動(dòng)被ScrollView消費(fèi),導(dǎo)致無(wú)法觸動(dòng)星星的滑動(dòng)。所以為了解決這個(gè)問(wèn)題,就需要處理滑動(dòng)沖突。
可以看到這里重寫(xiě)了dispathTouchEvent(),利用requestDisallowInterceptTouchEvent(true)進(jìn)行攔截。這里的處理邏輯就是,當(dāng)DOWN事件時(shí)進(jìn)行攔截,交給星星處理,當(dāng)是MOVE事件時(shí),對(duì)滑動(dòng)的X和Y軸的方向的距離進(jìn)行判斷,如果X>Y,則進(jìn)行攔截,如果Y>X,則交給ScrollView處理。
總結(jié)
這里大體的實(shí)現(xiàn)思路分析完了,總的來(lái)說(shuō)沒(méi)有特別的難點(diǎn),主要就是處理自定義組件的細(xì)節(jié)問(wèn)題需要多注意,多在細(xì)節(jié)進(jìn)行優(yōu)化。這里提供Github地址https://github.com/sdfdzx/CustomRatingBar/blob/master/library/src/main/java/com/study/dzx/library/widget/CustomAnimRatingBar.java