English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
I saw a component called PendulumView for IOS on the Internet, which implements the pendulum animation effect. Since the native progress bar is really ugly, I want to customize the View to achieve such an effect, which can also be used for the progress bar of the loading page in the future.
No more nonsense, let's show the effect first
The bottom black edge is accidentally recorded during recording and can be ignored.
Since it is a custom View, we will follow the standard process, the first step, custom attribute
Custom attribute
Create attribute file
In Android project's res->values directory, create a new attrs.xml file, the content of the file is as follows:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="PendulumView"> <attr name="globeNum" format="integer"/> <attr name="globeColor" format="color"/> <attr name="globeRadius" format="dimension"/> <attr name="swingRadius" format="dimension"/> </declare-styleable> </resources>
L'attributo name della declare-styleable viene utilizzato per fare riferimento a questo file di proprietà nel codice. Di solito, l'attributo name viene scritto con il nome della nostra classe View personalizzata, il che è piuttosto intuitivo.
Utilizzando styleale, il sistema può completare molte scritture di costanti (array di int, costanti di indici) per noi, semplificando il nostro lavoro di sviluppo, ad esempio, il codice di seguito utilizza R.styleable.PendulumView_golbeNum che viene generato automaticamente dal sistema per noi.
L'attributo globeNum rappresenta il numero di palline, globeColor rappresenta il colore delle palline, globeRadius rappresenta il raggio delle palline, swingRadius rappresenta il raggio di oscillazione
Leggere i valori delle proprietà
Leggere i valori delle proprietà nel costruttore della view personalizzata
È possibile ottenere i valori delle proprietà anche tramite AttributeSet, ma se il valore della proprietà è di tipo riferimento, si ottiene solo l'ID, è necessario continuare a decodificare l'ID per ottenere il valore effettivo della proprietà, mentre TypedArray ci aiuta a completare questo lavoro.
public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //Utilizzare TypedArray per leggere i valori delle proprietà personalizzate TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView); int count = ta.getIndexCount(); for (int i = 0; i < count; i++) { int attr = ta.getIndex(i); switch (attr) { case R.styleable.PendulumView_globeNum: mGlobeNum = ta.getInt(attr, 5); break; case R.styleable.PendulumView_globeRadius: mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; case R.styleable.PendulumView_globeColor: mGlobeColor = ta.getColor(attr, Color.BLUE); break; case R.styleable.PendulumView_swingRadius: mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; } } ta.recycle(); // evita problemi durante la lettura successiva mPaint = new Paint(); mPaint.setColor(mGlobeColor); }
Riscrivi il metodo OnMeasure()
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //高度为小球半径+摆动半径 int height = mGlobeRadius + mSwingRadius; //宽度为2*摆动半径+(小球数量-1)*小球直径 int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius; //如果测量模式为EXACTLY,则直接使用推荐值,如不为EXACTLY(一般处理wrap_content情况),使用自己计算的宽高 setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height); }
其中
int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
用于处理测量模式为AT_MOST的情况,一般是自定义View的宽高设置为了wrap_content,此时通过小球的数量,半径,摆动的半径等计算View的宽高,如下图:
以小球个数5为例,View的大小为下图红色矩形区域
重写onDraw()方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制除左右两个小球外的其他小球 for (int i = 0; i < mGlobeNum - 2; i++) { canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint); } if (mLeftPoint == null || mRightPoint == null) { //初始化最左右两小球坐标 mLeftPoint = new Point(mSwingRadius, mSwingRadius); mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius); //Avvia l'animazione di oscillazione startPendulumAnimation(); } //Disegna i due pallini di sinistra e di destra canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint); canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint); }
Il metodo onDraw() è la chiave di una View personalizzata, all'interno del quale si disegna l'effetto visivo della View. Il codice disegna prima tutti i pallini tranne quelli estremi di sinistra e destra, quindi giudica le coordinate dei pallini di sinistra e di destra. Se è la prima volta che si disegna, le coordinate sono vuote, allora inizializza le coordinate dei due pallini e avvia l'animazione. Infine, attraverso i valori x, y di mLeftPoint, mRightPoint, disegna i due pallini.
In cui mLeftPoint, mRightPoint sono oggetti di android.graphics.Point, utilizzati solo per memorizzare le informazioni sulle coordinate x, y dei pallini di sinistra e di destra.
Utilizza l'animazione delle proprietà
public void startPendulumAnimation() { //Utilizza l'animazione delle proprietà final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值 double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point()); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Point point = (Point) animation.getAnimatedValue(); //Ottieni il valore di fraction corrente float fraction = anim.getAnimatedFraction(); //Determina se fraction diminuisce prima di aumentare, ovvero se si trova in uno stato di oscillazione verso l'alto //Passa al pallino in movimento ogni volta che sta per oscillare verso l'alto if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //Attraverso la continua modifica delle coordinate x, y dei pallini di sinistra e di destra si realizza l'effetto animato //Utilizza isNext per determinare se dovrebbe muoversi il pallino di sinistra o quello di destra if (isNext) { //Quando il pallino di sinistra si muove, il pallino di destra viene posizionato in posizione iniziale mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1); mRightPoint.y = mSwingRadius; mLeftPoint.x = mSwingRadius - point.x; mLeftPoint.y = mGlobeRadius + point.y; } else { //当右边小球摆动时,左边小球置于初始位置 mLeftPoint.x = mSwingRadius; mRightPoint.y = mSwingRadius; mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x; mRightPoint.y = mGlobeRadius + point.y; } invalidate(); lastSlope = fraction < mLastFraction; mLastFraction = fraction; } }); //设置永久循环播放 anim.setRepeatCount(ValueAnimator.INFINITE); //设置循环模式为倒序播放 anim.setRepeatMode(ValueAnimator.REVERSE); anim.setDuration(200); //设置补间器,控制动画的变化速率 anim.setInterpolator(new DecelerateInterpolator()); anim.start(); }
其中使用ValueAnimator.ofObject方法是为了可以对Point对象进行操作,更为形象具体。还有就是通过ofObject方法使用了自定义的TypeEvaluator对象,由此得到了fraction值,该值是一个从0-1变化的小数。所以该方法的后两个参数startValue(new Point()),endValue(new Point())并没有实际意义,也可以直接不写,此处写上主要是为了便于理解。同样道理也可以直接使用ValueAnimator.ofFloat(0f, 1f)方法获取到一个从0-1变化的小数。
final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值 double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point());
Tramite fraction, calcoliamo il valore di variazione dell'angolo dell'oscillazione del pallino, da 0 a 90 gradi
Il valore rappresentato da mSwingRadius - mGlobeRadius è la lunghezza della linea verde nell'immagine, la traiettoria dell'oscillazione, la traiettoria del centro del pallino è un arco di cerchio con un raggio di (mSwingRadius - mGlobeRadius), il valore di X variabile è (mSwingRadius - mGlobeRadius) * sin(angle), il valore di Y variabile è (mSwingRadius - mGlobeRadius) * cos(angle)
Le coordinate effettive del centro della sfera del pallino corrispondente sono (mSwingRadius - x, mGlobeRadius + y)
La traiettoria del pallino di destra è simile a quella del pallino di sinistra, ma con direzione opposta. Le coordinate effettive del centro della sfera del pallino di destra sono (mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x, mGlobeRadius + y)
E' visibile che le coordinate y dei pallini sui lati sinistro e destro sono le stesse, mentre le coordinate x sono diverse.
float fraction = anim.getAnimatedFraction(); //Determina se fraction diminuisce prima di aumentare, ovvero se si trova in uno stato di oscillazione verso l'alto //Passa al pallino in movimento ogni volta che sta per oscillare verso l'alto if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //Registra se l'ultima fraction è continuamente diminuita lastSlope = fraction < mLastFraction; //Registra la fraction dell'ultima volta mLastFraction = fraction;
Questi due segmenti di codice servono per calcolare quando passare al pallino in movimento, questa animazione è impostata per ripetere in modo ciclico e in ordine inverso, quindi un ciclo dell'animazione è il processo di lancio e caduta del pallino. Durante questo processo, il valore di fraction passa da 0 a 1 e poi da 1 a 0. Allora quando inizia un nuovo ciclo dell'animazione? È quando il pallino sta per essere lanciato, in questo momento passare al pallino in movimento può realizzare l'effetto dell'animazione in cui il pallino di sinistra cade e il pallino di destra viene lanciato, e il pallino di destra cade e il pallino di sinistra viene lanciato.
那么如何捕捉到这个时间点呢?
小球抛起时fraction值不断增大,小球落下时fraction值不断减小。小球即将抛起的时刻,就是fraction从不断减小转变为不断增大的时刻。代码中记录上一次fraction是否在不断减小,然后比较这一次fraction是否在不断增大,若两个条件均成立则切换运动的小球。
anim.setDuration(200); //设置补间器,控制动画的变化速率 anim.setInterpolator(new DecelerateInterpolator()); anim.start();
设置动画的持续时间为200毫秒,读者可以通过更改该值而达到修改小球摆动速度的目的。
设置动画的补间器,由于小球抛起是一个逐渐减速的过程,落下是一个逐渐加速的过程,所以使用DecelerateInterpolator实现减速效果,在倒序播放时为加速效果。
启动动画,钟摆效果的自定义View进度条就实现了!赶快运行,看看效果吧!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持呐喊教程。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#oldtoolbag.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。