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 ( 로 컬 다운로드 )
총결산
이상 은 이 글 의 전체 내용 입 니 다.본 논문 의 내용 이 여러분 의 학습 이나 업무 에 어느 정도 참고 학습 가치 가 있 기 를 바 랍 니 다.궁금 한 점 이 있 으 시 면 댓 글 을 남 겨 주 셔 서 저희 에 대한 지지 에 감 사 드 립 니 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Kotlin의 기초 - 2부지난 글에서는 Kotlin이 무엇인지, Kotlin의 특징, Kotlin에서 변수 및 데이터 유형을 선언하는 방법과 같은 Kotlin의 기본 개념에 대해 배웠습니다. 유형 변환은 데이터 변수의 한 유형을 다른 데이터...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.