Android 속성 애니메이션 을 사용 하여 카운트다운 컨트롤 을 사용자 정의 하 는 방법 에 대한 자세 한 설명

속성 애니메이션 을 왜 도입 합 니까?
Android 이전의 추가 애니메이션 체 제 는 비교적 건전 한 편 이다.android.view.animation 패키지 아래 에 우리 가 조작 할 수 있 는 여러 가지 유형 이 있어 서 일련의 애니메이션 효 과 를 완성 할 수 있다.예 를 들 어 View 에 대해 이동,확대,회전 과 페이드아웃 을 하고 우 리 는 AnimationSet 을 통 해 이런 애니메이션 효 과 를 조합 해서 사용 할 수 있다.그 밖 에 Interpolator 설정 을 통 해 애니메이션 의 재생 속 도 를 조절 할 수 있 습 니 다.그러면 여기 서 여러분 들 은 의문 이 생 길 것 입 니 다.예전 의 애니메이션 체제 가 이렇게 건전 한 이상 왜 속성 애니메이션 을 도입 해 야 합 니까?
사실은 위 에서 말 하 는 건전 성 은 모두 상대 적 인 것 입 니 다.만약 에 귀하 의 수요 에서 View 를 이동,확대,회전 과 페이드아웃 작업 만 하면 보충 애니메이션 은 충분히 건전 합 니 다.그러나 이러한 기능 은 모든 장면 을 커버 하기에 부족 하 다.만약 에 우리 의 수요 가 이동,확대,회전 과 페이드아웃 이라는 네 가지 View 에 대한 조작 을 초과 하면 보충 애니메이션 은 더 이상 우 리 를 도와 줄 수 없다.즉,기능 과 확장 가능 한 측면 에서 상당 한 한계점 을 가진다.그럼 보 간 애니메이션 이 감당 할 수 없 는 장면 을 살 펴 보 겠 습 니 다.
위 에서 제 가 보충 애니메이션 을 소개 할 때'View 에 대한 조작'이라는 묘 사 를 사 용 했 습 니 다.맞아요.보충 애니메이션 은 View 에 만 작용 할 수 있 습 니 다.즉,우 리 는 Button,TextView,심지어 LinearLayout,또는 View 를 계승 하 는 다른 구성 요소 에 대해 애니메이션 작업 을 할 수 있 습 니 다.그러나 우리 가 View 가 아 닌 대상 에 대해 애니메이션 작업 을 하려 면 죄 송 하지만 추가 애니메이션 은 도움 이 되 지 않 습 니 다.어떤 친구 들 은 이해 할 수 없다 고 느 낄 수 있 습 니 다.제 가 어떻게 비 View 대상 에 대해 애니메이션 작업 을 해 야 합 니까?여기 서 저 는 간단 한 예 를 들 어 사용자 정의 View 가 있 습 니 다.이 View 에서 Point 대상 이 좌 표를 관리 하 는 데 사 용 된 다음 에 onDraw()방법 에서 이 Point 대상 의 좌표 값 에 따라 그 렸 습 니 다.즉,우리 가 Point 대상 에 대해 애니메이션 작업 을 할 수 있다 면 전체 사용자 정의 View 의 애니메이션 효과 가 있 을 것 이다.분명히 보 간 애니메이션 은 이 기능 을 갖 추 지 못 한 것 이 첫 번 째 결함 이다.
그 다음 에 애니메이션 을 보충 하 는 데 또 하나의 결함 이 있 습 니 다.바로 이동,확대,회전 과 페이드아웃 등 네 가지 애니메이션 작업 만 실현 할 수 있다 는 것 입 니 다.만약 에 우리 가 View 의 배경 색 을 동태 적 으로 바 꾸 기 를 원한 다 면?유 감 스 럽 지만 우 리 는 스스로 실현 할 수 밖 에 없다.말하자면 이전의 보충 애니메이션 메커니즘 은 하 드 코딩 방식 으로 완 성 된 것 이 고 기능 한정 사 는 바로 이런 것 이다.기본적으로 확장 성 이 없다.
마지막 으로 보 간 애니메이션 은 또 하나의 치 명 적 인 결함 이 있다.바로 View 의 디 스 플레이 효 과 를 바 꾸 었 을 뿐 View 의 속성 을 진정 으로 바 꾸 지 않 는 다 는 것 이다.무슨 뜻 이 죠?예 를 들 어 현재 화면의 왼쪽 상단 에 버튼 이 있 습 니 다.그리고 우 리 는 보충 애니메이션 을 통 해 화면의 오른쪽 하단 으로 이동 시 켰 습 니 다.지금 은 이 단 추 를 눌 러 보 세 요.이 단 추 를 누 르 면 이벤트 가 절대 촉발 되 지 않 습 니 다.실제로 이 단 추 는 화면의 왼쪽 상단 에 머 물 러 있 기 때 문 입 니 다.이 단 추 를 화면의 오른쪽 아래 에 보 간 애니메이션 으로 그 렸 을 뿐 입 니 다.
바로 이런 이유 로 안 드 로 이 드 개발 팀 은 3.0 버 전에 속성 애니메이션 이라는 기능 을 도입 하기 로 결정 했다.그러면 속성 애니메이션 은 상기 문 제 를 모두 해결 하 는 것 이 아 닐 까?다음은 우리 한번 봅 시다.
새로 도 입 된 속성 애니메이션 체 제 는 더 이상 View 를 대상 으로 디자인 된 것 이 아니 라 이동,확대,회전 과 페이드아웃 등 몇 가지 애니메이션 작업 만 실현 하 는 것 에 국한 되 지 않 고 시각 적 인 애니메이션 효과 도 아니다.이것 은 실제 적 으로 값 을 끊임없이 조작 하 는 메커니즘 이 고 값 을 지정 한 대상 의 지정 속성 에 부여 하 며 임의의 대상 의 임 의 속성 일 수 있다.그래서 저 희 는 하나의 View 를 이동 하거나 확대 할 수 있 지만 사용자 정의 View 의 Point 대상 에 대해 애니메이션 작업 도 할 수 있 습 니 다.우 리 는 시스템 애니메이션 의 운행 시간 이 길 고 어떤 유형의 애니메이션 을 실행 해 야 하 는 지,그리고 애니메이션 의 초기 값 과 끝 값 만 알려 주면 나머지 작업 은 모두 시스템 에 맡 길 수 있다.
속성 애니메이션 의 실현 체 제 는 목표 대상 에 대한 할당 과 속성 을 수정 함으로써 이 루어 진 것 이기 때문에 앞에서 말 한 버튼 에 나타 난 문제 도 존재 하지 않 습 니 다.만약 에 우리 가 속성 애니메이션 을 통 해 단 추 를 이동 하면 이 단 추 는 다른 위치 에서 만 그 려 진 것 이 아니 라 진정한 이동 입 니 다.
자,이렇게 많이 소 개 했 습 니 다.여러분 들 이 속성 애니메이션 에 대해 가장 기본 적 인 인식 을 가지 게 되 었 다 고 믿 습 니 다.다음은 상세 한 소 개 를 보 겠 습 니 다.
머리말
본 고 는 속성 애니메이션(Timer 를 사용 하지 않 고 애니메이션 실행 횟수 를 통 해 카운트다운 을 제어 합 니 다)을 이용 하여 원형 카운트다운 컨트롤 을 사용자 정의 하 는 것 을 소개 합 니 다.비교적 초라 합 니 다.예제 만 사용 하고 필요 하 다 면 사용자 가 직접 수정 하여 수 요 를 만족 시 킬 수 있 습 니 다.컨트롤 에서 사용 하 는 소재 와 배색 은 모두 필자 가 마음대로 선택 하여 효과 가 좋 지 않 으 므 로 먼저 예시 사진 을 올 립 니 다.

예제 에서 진도 바 바탕색,그 라 데 이 션 색(두 색 값 만 지원),글꼴 크기,그림,진도 바 너비 와 진도 바 표시 여부 등 은 xml 을 통 해 수정 할 수 있 고 카운트다운 시간 은 코드 를 통 해 설정 할 수 있 습 니 다.관심 이 있 으 시 면 코드 설정 이 더욱 풍부 한 그 라 데 이 션 색상 값 과 문자 변화 효 과 를 수정 할 수 있 습 니 다.본 고 는 디자인 방향 만 제공 합 니 다.
필 자 는 속성 애니메이션 을 이용 하여 여러 번 카운트다운 을 실 행 했 고 실행 횟수 는 카운트다운 초기 수치 이다.상술 한 예 시 를 해체 하면 실현 하기 가 여전히 매우 쉬 운 것 을 발견 할 수 있 으 며,처리 해 야 할 것 은 주로 다음 과 같은 몇 부분 이다.
1.외부 링 진도 막대 그리 기
2.중앙 회전 그림 그리 기
3.카운트다운 시간 그리 기
1.외부 링 진도 바 를 그립 니 다.두 부분 으로 나 눕 니 다.
1.링 배경 canvas.drawCircle 방법 그리 기
2.부채 형 진도 canvas.drawArc 방법 으로 그립 니 다.호 도 는 전체 카운트다운 을 통 해 진도 제 어 를 수행 합 니 다.
2.중앙 회전 그림 그리 기:
선행 설명:바깥쪽 원형 지름 은 d1 로 설정 합 니 다.중앙 회전 그림 의 직경 을 d2 로 설정 합 니 다.진도 바 폭 d3 로 설정
1.설 정 된 그림 을 잘라 서 크기 를 조정 합 니 다(잘라 내지 않 아 도 됩 니 다.필 자 는 강박 증 이 있 습 니 다).너비 가 d1-2*d3,즉 d2=d1-2*d3 와 같 도록 합 니 다.
2.Matrix 를 이용 하여 Bitmap 를 중앙 으로 이동 합 니 다.
3.매트릭스 로 비트 맵 회전 하기
3.카운트다운 시간 그리 기:
매번 애니메이션 실행 진 도 를 통 해 텍스트 위 치 를 제어 합 니 다.
아래 예시 코드:

public class CircleCountDownView extends View {
 private CountDownListener countDownListener;

 private int width;
 private int height;
 private int padding;
 private int borderWidth;
 //                ,        ,       0 1
 private float currentAnimationInterpolation;
 private boolean showProgress;
 private float totalTimeProgress;
 private int processColorStart;
 private int processColorEnd;
 private int processBlurMaskRadius;

 private int initialCountDownValue;
 private int currentCountDownValue;

 private Paint circleBorderPaint;
 private Paint circleProcessPaint;
 private RectF circleProgressRectF;

 private Paint circleImgPaint;
 private Matrix circleImgMatrix;
 private Bitmap circleImgBitmap;
 private int circleImgRadius;
 private AnimationInterpolator animationInterpolator;
 private BitmapShader circleImgBitmapShader;
 private float circleImgTranslationX;
 private float circleImgTranslationY;
 private Paint valueTextPaint;

 private ValueAnimator countDownAnimator;

 public CircleCountDownView(Context context) {
 this(context, null);
 }

 public CircleCountDownView(Context context, @Nullable AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public CircleCountDownView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 setLayerType(View.LAYER_TYPE_SOFTWARE, null);
 init(attrs);
 }

 private void init(AttributeSet attrs) {
 circleImgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 circleImgPaint.setStyle(Paint.Style.FILL);
 circleImgMatrix = new Matrix();
 valueTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

 TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircleCountDownView);
 //           
 padding = typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_padding, DisplayUtil.dp2px(5));
 //        
 borderWidth = typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_circleBorderWidth, 0);
 if (borderWidth > 0) {
  circleBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  circleBorderPaint.setStyle(Paint.Style.STROKE);
  circleBorderPaint.setStrokeWidth(borderWidth);
  circleBorderPaint.setColor(typedArray.getColor(R.styleable.CircleCountDownView_circleBorderColor, Color.WHITE));

  showProgress = typedArray.getBoolean(R.styleable.CircleCountDownView_showProgress, false);
  if (showProgress) {
  circleProcessPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  circleProcessPaint.setStyle(Paint.Style.STROKE);
  circleProcessPaint.setStrokeWidth(borderWidth);
  //        
  processColorStart = typedArray.getColor(R.styleable.CircleCountDownView_processColorStart, Color.parseColor("#00ffff"));
  processColorEnd = typedArray.getColor(R.styleable.CircleCountDownView_processColorEnd, Color.parseColor("#35adc6"));
  //          
  processBlurMaskRadius = typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_processBlurMaskRadius, DisplayUtil.dp2px(5));
  }
 }


 int circleImgSrc = typedArray.getResourceId(R.styleable.CircleCountDownView_circleImgSrc, R.mipmap.ic_radar);
 //         
 circleImgBitmap = ImageUtil.cropSquareBitmap(BitmapFactory.decodeResource(getResources(), circleImgSrc));

 valueTextPaint.setColor(typedArray.getColor(R.styleable.CircleCountDownView_valueTextColor, Color.WHITE));
 valueTextPaint.setTextSize(typedArray.getDimensionPixelSize(R.styleable.CircleCountDownView_valueTextSize, DisplayUtil.dp2px(13)));

 typedArray.recycle();

 //        ,   1 
 countDownAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);
 countDownAnimator.setInterpolator(new LinearInterpolator());
 countDownAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  @Override
  public void onAnimationUpdate(ValueAnimator animation) {
  if (countDownListener != null) {
   //       
   long restTime = (long) ((currentCountDownValue - animation.getAnimatedFraction()) * 1000);
   countDownListener.restTime(restTime);
  }
  //        
  totalTimeProgress = (initialCountDownValue - currentCountDownValue + animation.getAnimatedFraction()) / initialCountDownValue;
  if (animationInterpolator != null) {
   currentAnimationInterpolation = animationInterpolator.getInterpolation(animation.getAnimatedFraction());
  } else {
   currentAnimationInterpolation = animation.getAnimatedFraction();
   currentAnimationInterpolation *= currentAnimationInterpolation;
  }
  invalidate();
  }
 });
 countDownAnimator.addListener(new AnimatorListenerAdapter() {
  @Override
  public void onAnimationRepeat(Animator animation) {
  currentCountDownValue--;
  }

  @Override
  public void onAnimationEnd(Animator animation) {
  if (countDownListener != null) {
   countDownListener.onCountDownFinish();
  }
  }
 });
 }

 //          
 public void setStartCountValue(int initialCountDownValue) {
 this.initialCountDownValue = initialCountDownValue;
 this.currentCountDownValue = initialCountDownValue;
 //         ,   initialCountDownValue ,        
 countDownAnimator.setRepeatCount(currentCountDownValue - 1);
 invalidate();
 }

 public void setAnimationInterpolator(AnimationInterpolator animationInterpolator) {
 if (!countDownAnimator.isRunning()) {
  this.animationInterpolator = animationInterpolator;
 }
 }

 //   
 public void reset() {
 countDownAnimator.cancel();
 lastAnimationInterpolation = 0;
 totalTimeProgress = 0;
 currentAnimationInterpolation = 0;
 currentCountDownValue = initialCountDownValue;
 circleImgMatrix.setTranslate(circleImgTranslationX, circleImgTranslationY);
 circleImgMatrix.postRotate(0, width / 2, height / 2);
 invalidate();
 }

 public void restart() {
 reset();
 startCountDown();
 }

 public void pause() {
 countDownAnimator.pause();
 }

 public void setCountDownListener(CountDownListener countDownListener) {
 this.countDownListener = countDownListener;
 }

 //      
 public void startCountDown() {
 if (countDownAnimator.isPaused()) {
  countDownAnimator.resume();
  return;
 }
 if (currentCountDownValue > 0) {
  countDownAnimator.start();
 } else if (countDownListener != null) {
  countDownListener.onCountDownFinish();
 }
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 width = getMeasuredWidth();
 height = getMeasuredHeight();
 if (width > 0 && height > 0) {
  doCalculate();
 }
 }

 private void doCalculate() {
 circleImgMatrix.reset();
 //           
 circleImgRadius = (Math.min(width, height) - 2 * borderWidth - 2 * padding) / 2;
 float actualCircleImgBitmapWH = circleImgBitmap.getWidth();
 float circleDrawingScale = circleImgRadius * 2 / actualCircleImgBitmapWH;
 // bitmap    
 Matrix matrix = new Matrix();
 matrix.setScale(circleDrawingScale, circleDrawingScale, actualCircleImgBitmapWH / 2, actualCircleImgBitmapWH / 2);
 circleImgBitmap = Bitmap.createBitmap(circleImgBitmap, 0, 0, circleImgBitmap.getWidth(), circleImgBitmap.getHeight(), matrix, true);
 //         
 circleImgBitmapShader = new BitmapShader(circleImgBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
 //      
 circleImgTranslationX = (width - circleImgRadius * 2) / 2;
 circleImgTranslationY = (height - circleImgRadius * 2) / 2;
 circleImgMatrix.setTranslate(circleImgTranslationX, circleImgTranslationY);

 if (borderWidth > 0) {
  //        (  :        )
  float circleProgressWH = Math.min(width, height) - borderWidth - 2 * padding;
  float left = (width > height ? (width - height) / 2 : 0) + borderWidth / 2 + padding;
  float top = (height > width ? (height - width) / 2 : 0) + borderWidth / 2 + padding;
  float right = left + circleProgressWH;
  float bottom = top + circleProgressWH;
  circleProgressRectF = new RectF(left, top, right, bottom);
  if (showProgress) {
  //               
  circleProcessPaint.setShader(new LinearGradient(left, top, left + circleImgRadius * 2, top + circleImgRadius * 2, processColorStart, processColorEnd, Shader.TileMode.MIRROR));
  circleProcessPaint.setMaskFilter(new BlurMaskFilter(processBlurMaskRadius, BlurMaskFilter.Blur.SOLID)); //          
  }
 }
 }

 private float lastAnimationInterpolation;

 @Override
 protected void onDraw(Canvas canvas) {
 if (width == 0 || height == 0) {
  return;
 }
 int centerX = width / 2;
 int centerY = height / 2;
 if (borderWidth > 0) { 
  //       
  canvas.drawCircle(centerX, centerY, Math.min(width, height) / 2 - borderWidth / 2 - padding, circleBorderPaint);
  if (showProgress) {
  //       
  canvas.drawArc(circleProgressRectF, 0, 360 * totalTimeProgress, false, circleProcessPaint);
  }

 }
 //           
 circleImgMatrix.postRotate((currentAnimationInterpolation - lastAnimationInterpolation) * 360, centerX, centerY);
 circleImgBitmapShader.setLocalMatrix(circleImgMatrix);
 circleImgPaint.setShader(circleImgBitmapShader);
 canvas.drawCircle(centerX, centerY, circleImgRadius, circleImgPaint);
 lastAnimationInterpolation = currentAnimationInterpolation;

 //        
 // current
 String currentTimePoint = currentCountDownValue + "s";
 float textWidth = valueTextPaint.measureText(currentTimePoint);
 float x = centerX - textWidth / 2;
 Paint.FontMetrics fontMetrics = valueTextPaint.getFontMetrics();
 //        (       )
 float verticalBaseline = (height - fontMetrics.bottom - fontMetrics.top) / 2;
 //            y   
 float y = verticalBaseline - currentAnimationInterpolation * (Math.min(width, height) / 2);
 valueTextPaint.setAlpha((int) (255 - currentAnimationInterpolation * 255));
 canvas.drawText(currentTimePoint, x, y, valueTextPaint);

 // next
 String nextTimePoint = (currentCountDownValue - 1) + "s";
 textWidth = valueTextPaint.measureText(nextTimePoint);
 x = centerX - textWidth / 2;
 y = y + (Math.min(width, height)) / 2;
 valueTextPaint.setAlpha((int) (currentAnimationInterpolation * 255));
 canvas.drawText(nextTimePoint, x, y, valueTextPaint);
 }

 public interface CountDownListener {
 /**
  *      
  */
 void onCountDownFinish();

 /**
  *        
  *
  * @param restTime     ,    
  */
 void restTime(long restTime);
 }

 public interface AnimationInterpolator {
 /**
  * @param inputFraction         ,    0 1
  */
 float getInterpolation(float inputFraction);
 }
}
사용자 정의 속성 은 다음 과 같 습 니 다.

<declare-styleable name="CircleCountDownView">
 <!--        -->
 <attr name="circleImgSrc" format="reference" />
 <attr name="circleBorderColor" format="color" />
 <attr name="circleBorderWidth" format="dimension" />
 <attr name="valueTextSize" format="dimension" />
 <attr name="valueTextColor" format="color" />
 <attr name="padding" format="dimension" />
 <attr name="showProgress" format="boolean" />
 <attr name="processColorStart" format="color" />
 <attr name="processColorEnd" format="color" />
 <attr name="processBlurMaskRadius" format="dimension" />
 </declare-styleable>
코드 가 간단 합 니 다.질문 이 있 으 시 면 댓 글 을 환영 합 니 다.
전체 코드:https://github.com/670832188/TestApp ( 로 컬 다운로드 )

총결산
이상 은 이 글 의 전체 내용 입 니 다.본 논문 의 내용 이 여러분 의 학습 이나 업무 에 어느 정도 참고 학습 가치 가 있 기 를 바 랍 니 다.궁금 한 점 이 있 으 시 면 댓 글 을 남 겨 주 셔 서 저희 에 대한 지지 에 감 사 드 립 니 다.

좋은 웹페이지 즐겨찾기