English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Android 커스텀 뷰를 사용하여 물면 상승 효과 구현

구현 결과는 다음과 같습니다:

구현 방향:

1원 중 물면 상승 효과 구현 방법: Paint의 setXfermode 속성을 PorterDuff.Mode.SRC_IN으로 설정하여 진행 위치의 사각형과 원의 교집합을 그려서 구현합니다

2물파 효과는 어떻게 만드는가: 베塞尔 곡선을 사용하여 파동 최대 높이를 동적으로 변경하여 "진행이 증가함에 따라 물파가 점점 작아지는 효과"를 구현합니다.

말이 많지 않습니다. 코드를 보겠습니다.

먼저 커스터마이zed 속성 값부터, 어떤 커스터마이zed 속성 값이 있을까요?

원의 배경 색상: circle_color, 진행의 색상: progress_color, 진행 텍스트의 색상: text_color, 진행 텍스트의 크기: text_size, 마지막으로: 파동 최대 높이: ripple_topheight

<declare-styleable name="WaterProgressView"> 
 <attr name="circle_color" format="color"/><!--원의 색상--> 
 <attr name="progress_color" format="color"/><!--진행의 색상--> 
 <attr name="text_color" format="color"/><!--텍스트의 색상--> 
 <attr name="text_size" format="dimension"/><!--텍스트 크기--> 
 <attr name="ripple_topheight" format="dimension"/><!--물 페이지 파동 최대 높이-->
</declare-styleable>

아래는 커스터마이zed View: WaterProgressView의 일부 코드입니다:

멤버 변수

public class WaterProgressView extends ProgressBar {
 //기본적인 원 배경 색상
 public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc;
 //기본적인 진행 색상
 public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC;66;
 //기본적인 텍스트 색상
 public static final int DEFAULT_TEXT_COLOR = 0xffffffff;
 //기본적인 텍스트 크기
 public static final int DEFAULT_TEXT_SIZE = 18;
 //기본적인 파동 최대 높이
 public static final int DEFAULT_RIPPLE_TOPHEIGHT = 10;
 private Context mContext;
 private Canvas mPaintCanvas;
 private Bitmap mBitmap;
 //画圆的画笔
 private Paint mCirclePaint;
 //画圆的画笔的颜色
 private int mCircleColor;
 //画进度的画笔
 private Paint mProgressPaint;
 //画进度的画笔的颜色
 private int mProgressColor ;
 //画进度的path
 private Path mProgressPath;
 //贝塞尔曲线波峰最大值
 private int mRippleTop = 10;
 //进度文字的画笔
 private Paint mTextPaint;
 //进度文字的颜色
 private int mTextColor;
 private int mTextSize = 18;
 //目标进度,也就是双击时处理任务的进度,会影响曲线的振幅
 private int mTargetProgress = 50;
 //监听双击和单击事件
 private GestureDetector mGestureDetector;
}

获取自定义属性值:

private void getAttrValue(AttributeSet attrs) { 
 TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView); 
 mCircleColor = ta.getColor(R.styleable.WaterProgressView_circle_color, DEFAULT_CIRCLE_COLOR);    
 mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color, DEFAULT_PROGRESS_COLOR); 
 mTextColor = ta.getColor(R.styleable.WaterProgressView_text_color, DEFAULT_TEXT_COLOR);  
 mTextSize = (int) ta.getDimension(R.styleable.WaterProgressView_text_size, DesityUtils.sp2px(mContext,DEFAULT_TEXT_SIZE)); 
 mRippleTop = (int)ta.getDimension(R.styleable.WaterProgressView_ripple_topheight,DesityUtils.dp2px(mContext,DEFAULT_RIPPLE_TOPHEIGHT)); 
 ta.recycle();
}

定义构造函数,注意 mProgressPaint.setXfermode

//当new该类时调用此构造函数
public WaterProgressView(Context context) { 
 this(context,null);
}
//当xml文件中定义该自定义View时调用此构造函数
public WaterProgressView(Context context, AttributeSet attrs) { 
 this(context, attrs,0);
}
public WaterProgressView(Context context, AttributeSet attrs, int defStyleAttr) { 
 super(context, attrs, defStyleAttr); 
 this.mContext = context; 
 getAttrValue(attrs); 
 //初始化画笔的相关属性 
 initPaint(); 
 mProgressPath = new Path(); 
}
private void initPaint() { 
 //初始化画圆的paint
 mCirclePaint = new Paint(); 
 mCirclePaint.setColor(mCircleColor); 
 mCirclePaint.setStyle(Paint.Style.FILL); 
 mCirclePaint.setAntiAlias(true); 
 mCirclePaint.setDither(true); 
 //진행을 그리는 펜을 초기화합니다
 mProgressPaint = new Paint(); 
 mProgressPaint.setColor(mProgressColor); 
 mProgressPaint.setAntiAlias(true); 
 mProgressPaint.setDither(true); 
 mProgressPaint.setStyle(Paint.Style.FILL); 
 //실제로 mProgressPaint는도 직사각형을 그립니다. PorterDuff.Mode.SRC_IN으로 xfermode를 설정하면 원과 진행 직사각형의 교집합이 표시되어 반원이 됩니다.
 mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 
 //진행 텍스트를 그리는 펜을 초기화합니다
 mTextPaint = new Paint(); 
 mTextPaint.setColor(mTextColor); 
 mTextPaint.setStyle(Paint.Style.FILL); 
 mTextPaint.setAntiAlias(true); 
 mTextPaint.setDither(true); 
 mTextPaint.setTextSize(mTextSize);
}

onMeasure() 메서드 코드:

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
 //사용할 때는 반드시 해당 View의 크기를 명확히 정의해야 합니다. 즉 MeasureSpec.EXACTLY로 설정합니다.
 int width = MeasureSpec.getSize(widthMeasureSpec); 
 int height = MeasureSpec.getSize(heightMeasureSpec); 
 setMeasuredDimension(width, height); 
 //Bitmap을 초기화하여 모든 drawCircle, drawPath, drawText가 bitmap이 있는 canvas에 draw되도록 하고, 그런 다음 onDraw 메서드의 canvas에 bitmap을 그려줍니다.
 //따라서 이 bitmap의 width, height는 left, top, right, bottom의 padding을 뺀 값이어야 합니다:
 mBitmap = Bitmap.createBitmap(width-getPaddingLeft()-getPaddingRight(),height- getPaddingTop()-getPaddingBottom(), Bitmap.Config.ARGB_8888); 
 mPaintCanvas = new Canvas(mBitmap);
}

그 다음은 핵심 부분, onDraw 메서드 내의 코드입니다. 먼저 Circle, 진도 표시줄, 진도 텍스트를自定义 canvas의 bitmap에 그리고, 그 bitmap을 onDraw 메서드 내의 canvas에 그립니다. drawCircle과 drawText은 어렵지 않지만, 중요한 것은 진도 표시줄을 어떻게 그릴지입니다. 물파 효과와 곡선이 있으므로 drawPath를 사용합니다:

drawPath의 흐름은 다음과 같습니다:


ratio 코드는 다음과 같습니다. 즉 ratio는 현재 진도가 총 진도의 백분율입니다:

float ratio = getProgress();*1.0f/getMax();

좌표는 B점에서 아래로와 오른쪽으로 정의되므로, A점의 좌표는 (width,1-ratio)*height),그 중 width는 bitmap의 너비이고 height는 bitmap의 높이입니다. 먼저 mProgressPath.moveTo를 A점으로 하고, 그 다음 A점에서 시계 방향으로 path의 각 중요한 점을 결정합니다. 그림과 같이 코드는 다음과 같습니다:

int rightTop = (int) ((1-ratio)*height);
mProgressPath.moveTo(width,rightTop);
mProgressPath.lineTo(width,height);
mProgressPath.lineTo(0,height);
mProgressPath.lineTo(0,rightTop);

이렇게 mProgressPath는 C점으로 lineTo를 했으므로, A점과 C점 사이에 물파 효과를 형성해야 합니다. 따라서 A점과 C점 사이에 베塞尔 곡선을 그려야 합니다:


우리는 최고 파동고를 다음과 같이 설정합니다:10그래서 일단 길이는 다음과 같습니다:40, 그래서 width를 그리려면:*1.0f/40단계와 같은 곡선을 그리려면, 곡선을 그리는 코드는 다음과 같습니다:

int count = (int) Math.ceil(width*1.0f/(10 *4));
for(int i=0; i<count; i++) { 
 mProgressPath.rQuadTo(10,10,2* 10,0);
 mProgressPath.rQuadTo(10,-10,2* 10,0);  
}
mProgressPath.close();
mPaintCanvas.drawPath(mProgressPath,mProgressPaint);


이렇게면 물면이 상승하면서 파동이 있고 파동 효과가 있는 진도 표시줄을 그릴 수 있습니다. 하지만 물면이 상승할수록 목표 진도에 접근할수록 물면 파동이 점점 작아져야 합니다. 따라서 다음과 같이 해야 합니다:10변수로 정의된 mRippleTop과 같은 초기 최대 파동고를 추출한 후, top을 목표 진도에 점점 접근하는 진도에 따라 실시간 파동고로 정의합니다. mTargetProgress는 목표 progress입니다. 목표 진도가 있어야 현재 진도가 목표 진도에 점점 접근하는 과정에서 물면이 평면에 점점 접근하는 효과를 실현할 수 있습니다:

float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;

따라서 drawPath 코드는 다음과 같이 업데이트됩니다:

float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
for(int i=0; i<count; i++) { 
 mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
 mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0); 
}

이렇게 해서 물면이 상승하는 진도 막대를 정말로 구현할 수 있습니다.

그렇다면 어떻게 그림에서 양수 때 물면이 0%에서 목표 진도로 상승하고, 한 번 탭할 때 물면이 목표 진도에서 계속 움직이는 효과를 구현할 수 있을까요?
먼저 양수 효과의 구현에 대해 설명드리겠습니다: 이는 간단합니다. Handler를 정의하고, 양수할 때 handler.postDelayed(runnable, time)를 호출하여 일정 시간 간격으로 progress를 업데이트합니다.+1runnable에서 invalidate()를 통해 계속 진행 상황을 업데이트하고, 현재 progress가 mTargetProgress에 도달할 때까지 진행 상황을 업데이트합니다.

코드는 다음과 같습니다

/**
 * 양수 애니메이션을 구현합니다
 */
private void startDoubleTapAnimation() { 
 setProgress(0); 
 doubleTapHandler.postDelayed(doubleTapRunnable,60);
}
private Handler doubleTapHandler = new Handler(){ 
 @Override 
 public void handleMessage(Message msg) {  
 super.handleMessage(msg); 
 }
};
//양수 처리 스레드, 간격60ms마다 데이터를 보냅니다
private Runnable doubleTapRunnable = new Runnable() { 
 @Override 
 public void run() {  
 if(getProgress() < mTargetProgress) {   
  invalidate();   
  setProgress(getProgress());+1);   
  doubleTapHandler.postDelayed(doubleTapRunnable,60);  
 }   
  doubleTapHandler.removeCallbacks(doubleTapRunnable);  
 } 
 }
};

양수 효과가 구현되었으므로, 한 번 탭 효과를 어떻게 구현할 수 있을까요? 한 번 탭할 때 물면이 일정 시간 동안 계속 움직이고 물면 파동이 점점 작아지면 물면이 평평해집니다. 물면이 움직이는 횟수를 mSingleTapAnimationCount 변수로 정의하고, 양수와 같은 처리와 같이 Handler를 정의하여 일정 시간 간격으로 메시지를 보내면 됩니다.-- 그런 다음, 초기의 파동 고비를 한 번은 양수로 한 번은 음수로 대체하여 물면의 움직임 효과를 구현할 수 있습니다.

核心代码如下:

private void startSingleTapAnimation() { 
 isSingleTapAnimation = true; 
 singleTapHandler.postDelayed(singleTapRunnable,200);
}
private Handler singleTapHandler = new Handler(){ 
 @Override 
 public void handleMessage(Message msg) {  
 super.handleMessage(msg); 
 }
};
//단일 클릭 처리 스레드는 간격200ms마다 데이터를 전송합니다
private Runnable singleTapRunnable = new Runnable() { 
 @Override 
 public void run() {  
 if(mSingleTapAnimationCount > 0) {   
  invalidate();   
  mSingleTapAnimationCount--;   
  singleTapHandler.postDelayed(singleTapRunnable,200);  
 }   
  singleTapHandler.removeCallbacks(singleTapRunnable);  
 //단일 클릭 애니메이션 진행 중인지 여부  
  isSingleTapAnimation = false;   
 //단일 클릭 애니메이션 실행 횟수를 초기화합니다50번
  mSingleTapAnimationCount = 50;  
 } 
 }
};

onDraw에서의 코드는 단일 클릭과 더블 클릭 시 drawPath에서 곡선 부분의 그리기 로직이 다르기 때문에, isSingleTapAnimation이라는 변수를 정의하여 단일 클릭 애니메이션과 더블 클릭 애니메이션 중 어느 것을 진행하고 있는지 구분합니다.

변경된 코드는 다음과 같습니다:

//진도를 그립니다
mProgressPath.reset();
//오른쪽 상단에서 시작하여 경로를 그립니다
int rightTop = (int) ((1-ratio)*height);
mProgressPath.moveTo(width,rightTop);
mProgressPath.lineTo(width,height);
mProgressPath.lineTo(0,height);
mProgressPath.lineTo(0,rightTop);
//베塞尔 곡선을 그려, 파도선을 형성합니다
int count = (int) Math.ceil(width*1.0f/(mRippleTop *4));
//단일 클릭 애니메이션 상태가 아니며
if(!isSingleTapAnimation&&getProgress()>0) { 
 float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop; 
 for(int i=0; i<count; i++) {  
 mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);   
 mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0); 
 }
} 
 //배2동시에 짝수 시 곡선 방향은 그림과 같습니다. 홀수 시 곡선은 반대로 됩니다
 //float top = (mSingleTapAnimationCount 
 0)*1.0f/5홀수와 짝수 시 곡선 전환*10; 
 //if(mSingleTapAnimationCount% 
 ==0) {2else {  
  for(int i=0; i<count; i++) {   
  mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);    
  mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);   
 } 
 }  
 for(int i=0; i<count; i++) {   
  mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);    
  mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0); 
 } 
 }
}
mProgressPath.close();
mPaintCanvas.drawPath(mProgressPath,mProgressPaint);

기본적으로 중요한 코드와 핵심 로직은 위에 있습니다.

주의사항:

1、drawCircle할 때 padding을 고려해야 하면 circle의 너비와 높이는 getWidth와 getHeight에서 padding 값을 뺀 값입니다. 코드는 다음과 같습니다:

//bitmap의 너비와 높이를 정의합니다
int width = getWidth()-getPaddingLeft()-getPaddingRight();
int height = getHeight()-getPaddingTop()-getPaddingBottom();
//원을 그립니다
mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);

2、drawText할 때 text의 height의 중간에서 시작하는 것이 아니라 baseline에서 시작합니다

그럼 baseline의 height 좌표를 어떻게 얻을 수 있을까요

Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
//ascent는 baseline 위에 있으므로 ascent는 음수입니다. descent+ascent가 음수이므로, 뺄 것이 아니라 더합니다
float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;

drawText의 전체 코드는 다음과 같습니다:

//그래프 진도 텍스트
String text = ((int)(ratio*100))+"%";
//문자의 너비를 얻습니다
float textWidth = mTextPaint.measureText(text);
Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
//descent+ascent가 음수이므로, 뺄 것이 아니라 더합니다
float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
mPaintCanvas.drawText(text,width/2-textWidth/2,baseLine,mTextPaint);

3padding을 고려하여, onDraw에서 canvas를 (getPaddingLeft(),getPaddingTop()) 위치로 translate합니다.

canvas.translate(getPaddingLeft(),getPaddingTop());
canvas.drawBitmap(mBitmap,0,0,null);

마지막으로, 사용자 정의 bitmap을 onDraw의 canvas에 그려야 합니다. 여기까지 사용자 정의 물결 효과의 진행 바를 작성했습니다.

결론

이것이 이 기사의 전체 내용입니다. 이 기사의 내용이 여러분의 학습이나 업무에 도움이 되길 바랍니다. 의문이 있으시면 댓글을 통해 교류해 주세요.

언급: 본 내용은 인터넷에서 가져왔으며, 저작권자에게 속하며, 인터넷 사용자가 자발적으로 기여하고 업로드한 내용입니다. 이 사이트는 소유권을 가지지 않으며, 인공 편집을 하지 않았으며, 관련 법적 책임도 부담하지 않습니다. 저작권 침해가 의심되는 내용이 발견되면 notice#w로 이메일을 보내 주시기 바랍니다.3codebox.com(보고할 때는 #을 @으로 변경하십시오.)를 통해 신고하시고 관련 증거를 제공하시면, 사실이 확인되면 해당 사이트는 즉시 저작권 침해 내용을 삭제합니다.

좋아하는 것