English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
El efecto de implementación es el siguiente:
Enfoque de implementación:
1¿Cómo lograr el efecto de subida del nivel del agua en un círculo?: Utiliza la propiedad setXfermode de Paint para dibujar la intersección del rectángulo en el que se encuentra el progreso con el círculo
2¿Cómo lograr el efecto de ondas de agua: utilizando curvas de Bézier, cambiar dinámicamente el valor del pico de onda para lograr el efecto de que "a medida que aumenta el progreso, las ondas de agua se vuelven más pequeñas"
Sin más preámbulos, veamos el código.
Primero, los valores de atributos personalizados, ¿cuáles son los valores de atributos personalizados disponibles?
el color de fondo del círculo: circle_color, el color del progreso: progress_color, el color del texto del progreso: text_color, el tamaño del texto del progreso: text_size, y el último: la altura máxima de la onda: ripple_topheight
<declarar-styleable name="WaterProgressView"> <attr name="circle_color" format="color"/><!--color del círculo--> <attr name="progress_color" format="color"/><!--color de progreso--> <attr name="text_color" format="color"/><!--color de texto--> <attr name="text_size" format="dimension"/><!--tamaño de texto--> <attr name="ripple_topheight" format="dimension"/><!--altura máxima de la onda del agua--> </declarar-styleable>
a continuación se muestra parte del código personalizado de la vista personalizada: WaterProgressView
variables miembro
public class WaterProgressView extends ProgressBar { //color de fondo del círculo por defecto public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc; //color de progreso por defecto public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC;66; //color de texto por defecto public static final int DEFAULT_TEXT_COLOR = 0xffffffff; //tamaño de texto por defecto public static final int DEFAULT_TEXT_SIZE = 18; //punto más alto de la onda por defecto 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); //Inicializa el pincel para dibujar el progreso mProgressPaint = new Paint(); mProgressPaint.setColor(mProgressColor); mProgressPaint.setAntiAlias(true); mProgressPaint.setDither(true); mProgressPaint.setStyle(Paint.Style.FILL); //En realidad, mProgressPaint también dibuja un rectángulo, cuando se configura el xfermode a PorterDuff.Mode.SRC_IN, se muestra la intersección del círculo y el rectángulo de progreso, por lo que es un semicírculo mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //Inicializa el pincel para escribir texto de progreso mTextPaint = new Paint(); mTextPaint.setColor(mTextColor); mTextPaint.setStyle(Paint.Style.FILL); mTextPaint.setAntiAlias(true); mTextPaint.setDither(true); mTextPaint.setTextSize(mTextSize); }
Código del método onMeasure():
@Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //Al usarlo, es necesario definir claramente el tamaño de la View, es decir, usar el modo de medición MeasureSpec.EXACTLY int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width,height); //Inicializa Bitmap, permite que todos los drawCircle, drawPath, drawText se dibujen en el canvas del bitmap, y luego dibuja el bitmap en el canvas del método onDraw, //所以此bitmap的width,height需要减去left,top,right,bottom的padding Por lo tanto, el width y height de este bitmap necesitan restarse el padding de left, top, right, bottom-getPaddingLeft()-mBitmap = Bitmap.createBitmap(width- getPaddingTop()-getPaddingRight(),height8888); getPaddingBottom(), Bitmap.Config.ARGB_ }
mPaintCanvas = new Canvas(mBitmap);
A continuación, está la parte central, el código en onDraw. Primero dibujamos Circle, la barra de progreso y el texto de progreso en el bitmap de canvas personalizado, luego dibujamos este bitmap en el canvas de la método onDraw. No debería haber dificultad con drawCircle y drawText, el punto clave está en dibujar la barra de progreso, ¿cómo hacerlo? Ya que hay un efecto de ondas de agua y curvas, usamos drawPath.
El flujo de trabajo de drawPath es el siguiente:
Donde el código de ratio es el siguiente, es decir, ratio es el porcentaje de progreso actual del progreso total*1.0f/float ratio = getProgress();
getMax();1-ratio)*Debido a que las coordenadas se extienden en sentido positivo hacia abajo y hacia la derecha desde el punto B, las coordenadas del punto A son (width, (
int rightTop = (int) ((1-ratio)*height); mProgressPath.moveTo(width, rightTop); mProgressPath.lineTo(width, height); mProgressPath.lineTo(0, height); mProgressPath.lineTo(0, rightTop);
height),donde width es el ancho del bitmap y height es la altura del bitmap. Primero movemos mProgressPath.moveTo al punto A, luego desde el punto A se definen los puntos clave del path en sentido horario, como se muestra en la figura, entonces el código es el siguiente:
De esta manera, mProgressPath ya ha lineTo en el punto C, necesitando formar el efecto de ondas de agua entre el punto A y el punto C, por lo que se necesita dibujar una curva bezier entre el punto A y el punto C.10Nosotros establecemos el punto más alto de la cresta como4,entonces la longitud de onda de un segmento es*1.0f/40, se necesita dibujar width
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);
De esta manera, se puede dibujar una barra de progreso con el efecto de que el nivel del agua suba y tenga ondas. Pero aún debemos lograr que, a medida que el nivel del agua suba, al acercarse más al progreso objetivo, las ondas del agua deben ser cada vez más pequeñas, por lo que debe10Se extrae y se define como variable mRippleTop el valor máximo de la cresta de onda en el momento inicial, luego se define top como el valor real de la cresta de onda en tiempo real mientras se acerca progresivamente al progreso objetivo, donde mTargetProgress es el progreso objetivo, ya que solo con un progreso objetivo se puede lograr que el progreso actual se acerque progresivamente al progreso objetivo, con el efecto de que el nivel del agua se aligere hacia una superficie plana:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress*
因此drawPath的代码更新如下:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* 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+1,在runnable中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隔一段时间发送一次更新界面的message,mSingleTapAnimationCount-- ,然后我们交替地让初始时的波峰一次为正一次为负,从而实现水面涌动的效果。
核心代码如下:
private void startSingleTapAnimation() { isSingleTapAnimation = true; singleTapHandler.postDelayed(singleTapRunnable,200); } private Handler singleTapHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }; //hilo de manejo de clic, cada200ms para enviar datos una vez private Runnable singleTapRunnable = new Runnable() { @Override public void run() { if (mSingleTapAnimationCount > 0) { invalidate(); mSingleTapAnimationCount--; singleTapHandler.postDelayed(singleTapRunnable,200); } singleTapHandler.removeCallbacks(singleTapRunnable); //¿se está realizando una animación de clic? isSingleTapAnimation = false; //restablecer el número de veces que se ejecuta la animación de clic a50 veces mSingleTapAnimationCount = 50; } } };
modificar el código en onDraw, ya que la lógica de dibujo de la sección curva en drawPath es diferente según el clic y el doble clic, por lo que definimos una variable isSingleTapAnimation para distinguir entre si se realiza una animación de clic o una animación de doble clic.
el código modificado después de cambiar es el siguiente:
//dibujar el progreso mProgressPath.reset(); //dibujar la ruta desde la esquina superior derecha int rightTop = (int) ((1-ratio)*height); mProgressPath.moveTo(width, rightTop); mProgressPath.lineTo(width, height); mProgressPath.lineTo(0, height); mProgressPath.lineTo(0, rightTop); //dibujar la curva bezier, formando una onda int count = (int) Math.ceil(width*1.0f/(mRippleTop *4)) ; //no está en estado de animación de clic if (!isSingleTapAnimation && getProgress() > 0) { float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0); mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0); } } //estado de animación de clic, para ampliar el efecto, ampliar mRippleTop2veces //Al mismo tiempo, cuando es par, la dirección de la curva es como se muestra en la figura, y cuando es impar, la curva es exactamente al revés float top = (mSingleTapAnimationCount*1.0f/50)*10; //si 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);
Básicamente, el código importante y la lógica central se encuentran arriba.
Puntos a tener en cuenta:
1Al dibujar drawCircle, debe considerar el padding, por lo que el ancho y alto del círculo son width y height restados por el valor de padding, el código es el siguiente:
//definir el ancho y alto del bitmap personalizado int width = getWidth()-getPaddingLeft()-getPaddingRight(); int height = getHeight()-getPaddingTop()-getPaddingBottom(); //dibujar círculo mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);
2Al dibujar drawText, no se comienza a dibujar desde el medio de height del texto, sino desde la baseline
¿Cómo obtener la coordenada de height de baseline?
Paint.FontMetrics metrics = mTextPaint.getFontMetrics(); //Dado que ascent está por encima de baseline, ascent es un número negativo. descent+ascent es un número negativo, por lo que es restar en lugar de sumar float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
el código completo de drawText es el siguiente:
//progreso de dibujo de texto String text = ((int)(ratio*100))+"%"; //Obtener el ancho del texto float textWidth = mTextPaint.measureText(text); Paint.FontMetrics metrics = mTextPaint.getFontMetrics(); //descent+ascent es un número negativo, por lo que es restar en lugar de sumar float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2; mPaintCanvas.drawText(text,width/2-textWidth/2,baseLine,mTextPaint);
3Debido a que debe considerar el padding, recuerde que el canvas en onDraw debe traducirse a (getPaddingLeft(),getPaddingTop()).
canvas.translate(getPaddingLeft(),getPaddingTop()); canvas.drawBitmap(mBitmap,0,0,null);
Finalmente, recuerda dibujar el bitmap personalizado en el canvas de onDraw. Hasta aquí, he completado la barra de progreso de la superficie de agua personalizada.
Resumen
Esto es todo el contenido de este artículo, espero que el contenido de este artículo pueda ayudar a todos a aprender o trabajar, y si tienen alguna pregunta, pueden dejar comentarios para intercambiar.
Declaración: El contenido de este artículo se obtiene de la red, el derecho de autor pertenece al propietario original, el contenido se contribuye y carga de manera autónoma por los usuarios de Internet, este sitio web no posee los derechos de propiedad, no se ha procesado editorialmente y no asume ninguna responsabilidad legal relacionada. Si encuentra contenido sospechoso de violación de derechos de autor, por favor envíe un correo electrónico a: notice#w proporcionando evidencia relevante, y una vez que se verifique, este sitio eliminará inmediatamente el contenido sospechoso de violación de derechos de autor.3Declaración: El contenido de este artículo se obtiene de la red, el derecho de autor pertenece al propietario original, el contenido se contribuye y carga de manera autónoma por los usuarios de Internet, este sitio web no posee los derechos de propiedad, no se ha procesado editorialmente y no asume ninguna responsabilidad legal relacionada. Si encuentra contenido sospechoso de violación de derechos de autor, por favor envíe un correo electrónico a: notice#w proporcionando evidencia relevante, y una vez que se verifique, este sitio eliminará inmediatamente el contenido sospechoso de violación de derechos de autor.