Androidの練習シリーズ(13)、いくつかの興味深いカスタムビューの小さな栗を共有

栗のアルファ

これは、落下を模倣するピンの跳ね返り荷重効果です。以下を参照してください。

2021-04-25 at 13.37.03.gif

ピンを完全に引くことを考慮すると、Uiが普遍的ではなく、下部の「ロッド」効果がさまざまで変化するという問題が発生するためです。したがって、ここでのアイデアは、ピンを上部の円ビューと下部の「ロッド」ビットマップに分割することです。カスタム円のサイズと色を変更するだけで、Uiに最大限に適応できます。

ピンのロードアニメーションと下部リップル拡散効果は内部ハンドラーによって定期的に描画され、半径と色は毎回変更できます。メインコードの実装は次のとおりです。

読み込み中のアニメーションは次のとおりです。

    case MSG_TOP_DRAW:
        if (mTopCircleAnimaRadius < mTopCircleMaxRadius - 2 * topIntervalDistance) {
            mTopCircleAnimaRadius += topIntervalDistance;
        } else {
            mTopCircleAnimaRadius = mTopSmallCircleRadius + topIntervalDistance;
        } 

        drawTimingThread.removeMessages(MSG_TOP_DRAW);
        drawTimingThread.sendEmptyMessageDelayed(MSG_TOP_DRAW, animaTopIntervalTime);
        invalidate();
        break;
复制代码

これが下部のリップル拡散アニメーションです。

    case MSG_RIPPLE_DRAW:
       if (mBotCircleAnimaRadius < mBotCircleMaxRadius) {
            mBotCircleAnimaRadius += topIntervalDistance * 8;
            drawTimingThread.removeMessages(MSG_RIPPLE_DRAW);
            drawTimingThread.sendEmptyMessageDelayed(MSG_RIPPLE_DRAW, animaBotIntervalTime);
            // 透明度
            mBotCirclePaint.setAlpha(getAlphaOfRipple());
            invalidate();
        } else {
            mBotCircleAnimaRadius = 0;
            drawTimingThread.removeMessages(MSG_RIPPLE_DRAW);
        }
        break;
复制代码

ビューのジャンプアニメーションは、使用されるAnimatorSetのアニメーションを組み合わせたものであり、搭乗ポイントのドットテキスト効果は単純に描画され、詳細には展開されません。

    ...
    // translationY先上后下
    AnimatorSet mSet1 = new AnimatorSet();
    mSet1.play(mTAnimator1).before(mTAnimator2);
    mSet1.start();
复制代码

栗のベータ

このアニメーション効果は、円弧、ビットマップ、およびテキストの描画APIを使用して、ビューを連続的に描画することによって実現されます。目盛りの描画は、キャンバスを連続的に回転させることによって実現されます。

2021-04-25 at 22.41.42.gif

実装の難しさは、ラッピングプロセス中に周辺テキストの座標位置を確認することです。つまり、最終的な扇形の光線と中心座標、半径を通る円弧との交点のx、y座標を計算する方法です。 、およびセクター角度。幸いなことに、ソリューションはオンラインで見つけることができます。その背後にある数学モデルについては、以下のコードを参照してください。

    private void paintOutWord(Canvas canvas, String state) {
        PointF progressPoint = CommentUtil.calcArcEndPointXY
                (radius + getPaddingLeft() + specialScaleLineLength + scaleToRingSpace + wordWith
                        , radius + getPaddingTop() + specialScaleLineLength + scaleToRingSpace + wordHeigh
                        , radius + specialScaleLineLength + scaleToRingSpace
                        , progress * (360 / 100f), -90);
        int left = (int) progressPoint.x;
        int top = (int) progressPoint.y;
        wordPaint.getTextBounds(state, 0, state.length(), rect);
        if (left < radius + getPaddingLeft() + specialScaleLineLength + scaleToRingSpace + wordWith) {
            left -= rect.width();
        }
        if (top > radius + getPaddingTop() + specialScaleLineLength + scaleToRingSpace + wordHeigh) {
            top += rect.height();
        }
        canvas.drawText(state, left, top, wordPaint);
    }
复制代码

この方法の機能は、扇形の最終光線と円弧の交点のx、y座標を取得することです。興味がある場合は、次のことを学習できます。

    /**
     * @param cirX       圆centerX
     * @param cirY       圆centerY
     * @param radius     圆半径
     * @param cirAngle   当前弧角度
     * @param orginAngle 起点弧角度
     *
     * @return 扇形终射线与圆弧交叉点的xy坐标
     */
    public static PointF calcArcEndPointXY(float cirX, float cirY, float radius, 
            float cirAngle, float orginAngle) {
        cirAngle = (orginAngle + cirAngle) % 360;
        return calcArcEndPointXY(cirX, cirY, radius, cirAngle);
    }

    /*
     * @param cirAngle 当前弧角度
     */
    public static PointF calcArcEndPointXY(float cirX, float cirY, 
            float radius, float cirAngle) {
        float posX = 0.0f;
        float posY = 0.0f;
        // 将角度转换为弧度
        float arcAngle = (float) (Math.PI * cirAngle / 180.0);
        if (cirAngle < 90) {
            posX = cirX + (float) (Math.cos(arcAngle)) * radius;
            posY = cirY + (float) (Math.sin(arcAngle)) * radius;
        } else if (cirAngle == 90) {
            posX = cirX;
            posY = cirY + radius;
        } else if (cirAngle > 90 && cirAngle < 180) {
            arcAngle = (float) (Math.PI * (180 - cirAngle) / 180.0);
            posX = cirX - (float) (Math.cos(arcAngle)) * radius;
            posY = cirY + (float) (Math.sin(arcAngle)) * radius;
        } else if (cirAngle == 180) {
            posX = cirX - radius;
            posY = cirY;
        } else if (cirAngle > 180 && cirAngle < 270) {
            arcAngle = (float) (Math.PI * (cirAngle - 180) / 180.0);
            posX = cirX - (float) (Math.cos(arcAngle)) * radius;
            posY = cirY - (float) (Math.sin(arcAngle)) * radius;
        } else if (cirAngle == 270) {
            posX = cirX;
            posY = cirY - radius;
        } else {
            arcAngle = (float) (Math.PI * (360 - cirAngle) / 180.0);
            posX = cirX + (float) (Math.cos(arcAngle)) * radius;
            posY = cirY - (float) (Math.sin(arcAngle)) * radius;
        }
        return new PointF(posX, posY);
    }
复制代码

カラーグラデーション効果の実現は、各スケールに対応するカラーセグメントで同じ比率の16進カラー値を取得することです。コードは次のとおりです。

    /**
     * 通过刻度获取当前渐变颜色值
     * @param p 当前刻度
     * @param specialScaleCorlors 每个范围的颜色值
     * @return 当前需要的颜色值
     */
    public static int evaluateColor(int p, int[] specialScaleCorlors) {
        // 定义的颜色区间
        int startInt = 0xFFbebebe;
        int endInt = 0xFFbebebe;
        float fraction = 0.5f;
        
        if (p != 0 && p != 100) {
            startInt = specialScaleCorlors[p / 20];
            endInt = specialScaleCorlors[p / 20 + 1];
            fraction = (p - (p / 20) * 20) / 20f;
        }
        int startA = (startInt >> 24) & 0xff;
        int startR = (startInt >> 16) & 0xff;
        int startG = (startInt >> 8) & 0xff;
        int startB = startInt & 0xff;

        int endA = (endInt >> 24) & 0xff;
        int endR = (endInt >> 16) & 0xff;
        int endG = (endInt >> 8) & 0xff;
        int endB = endInt & 0xff;

        return (int) ((startA + (int) (fraction * (endA - startA))) << 24)
                | (int) ((startR + (int) (fraction * (endR - startR))) << 16)
                | (int) ((startG + (int) (fraction * (endG - startG))) << 8)
                | (int) ((startB + (int) (fraction * (endB - startB))));
    }
复制代码

残りの詳細と方法は掲載されません。それらはすべて従来のペイント方法です。

栗¶

この効果は上記と非常によく似ていますが、以下に示すように、このコントロールをドラッグしてスケールを選択できる点が異なります。

2021-04-25 at 22.34.12.gif

「ドラッグボタン」ビットマップを描画する場合、ビットマップの座標を決定すること、つまり、中心の座標に従ってファンの最終光線と円弧の交点のx座標とy座標を見つけることが困難です。このようにして、ビットマップの左上隅の座標を計算できます。

拖动效果是在我们允许的区域内,当手指按下,手指滑动,手指弹起时,不断绘制对应的进度p,给人一种圆环被拖着动画的错觉,其实这只是不断重绘的结果。这里需要我们通过onTouchEvent方法来监听手势及获取当前坐标。难点在于这是一个弧形轨迹,我们怎么通过当前坐标来获取角度,再根据角度获取相对应的进度。代码示例如下:

    @Override
    public synchronized boolean onTouchEvent(MotionEvent event) {
    
        int action = event.getAction();
        int x = (int) event.getX();
        int y = (int) event.getY();
        
        switch (action) {
            case MotionEvent.ACTION_DOWN:
            	// isOnRing 注释见下
                if (isOnRing(x, y) && y <= radius + getPaddingTop() + specialScaleLineLength + scaleToRingSpace) {
                    updateProgress(x, y);
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (y <= radius + getPaddingTop() + specialScaleLineLength + scaleToRingSpace) {
                    updateProgress(x, y);
                }
                return true;
            case MotionEvent.ACTION_UP:
                invalidate();
                break;
        }
        
        return super.onTouchEvent(event);
    }
复制代码

这是根据当前点的位置求角度,再转换成当前进度的方法:

    private void updateProgress(int eventX, int eventY) {
    
        double angle = Math.atan2(eventY - (radius + getPaddingLeft() + specialScaleLineLength + scaleToRingSpace)
                , eventX - (radius + getPaddingLeft() + specialScaleLineLength + scaleToRingSpace)) / Math.PI;
        angle = ((2 + angle) % 2 + (-beginLocation / 180f)) % 2;
        
        if ((int) Math.round(angle * 100) >= 0) {
            progress = (int) Math.round(angle * 100);
            realShowProgress = getShowProgress(progress);
        }
        
        invalidate();
    }
复制代码

需要注意的是,当我们拖动“拖动按钮”时,我们需要定一个特定的接收事件的区域范围,只有当用户按在了规定的可滑动区域内,才能让用户拖动进度条,并不是在任意位置都能拖动小图标改变进度的,这是判断当前触摸屏幕的位置是否处于可滑动区域内的方法:

    private boolean isOnRing(float eventX, float eventY) {
    
        boolean result = false;
        double distance = Math.sqrt(Math.pow(eventX - (radius + getPaddingLeft() + specialScaleLineLength + scaleToRingSpace), 2)
                + Math.pow(eventY - (radius+getPaddingLeft() + specialScaleLineLength + scaleToRingSpace), 2));
                
        if (distance < (2 * radius+getPaddingLeft() + getPaddingRight() + 2 * (specialScaleLineLength + scaleToRingSpace))
                && distance > radius - slideAbleLocation) {
            result = true;
        }
        
        return result;
    }
复制代码

其余的细节和方法就不贴了,都是比较常规的Paint方法。

栗子δ

这是一个波纹扩散、圆球旋转缩小的效果,具体见下:

2021-04-25 at 23.12.11.gif

这个效果是由一个整体的自定义View不断绘制而成。其中波纹扩散动画,是通过定时改变波纹半径来实现的,此波纹是由先后两个空心圆组成,在实现过程中要注意时间和各自的尺寸变化,核心代码见下:

    public void startAnima() {
        if (drawTimingThread != null) {
            drawTimingThread.sendEmptyMessage(MSG_DRAW0); // 开始1波纹
            float time = (mRMaxRadius - mRMinRadius) / distance * 0.5f; // 先取整,再取中
            drawTimingThread.sendEmptyMessageDelayed(MSG_DRAW1, (int)(animaBotIntervalTime * time));//定时开启2波纹
        }
    }
复制代码

这是波纹1的半径变化,参考代码如下:

    if (mCurRadius0 <= mRMaxRadius) {
        mCurRadius0 += distance;
    } else {
        mCurRadius0 = mRMinRadius + distance;
    }
    circlePointF0 = drawCircleOnRipple(MSG_DRAW0, curIndex0);

    mRPaint0.setAlpha(getAlphaOfRipple(curIndex0));//透明度
    mCirclePaint0.setAlpha(getAlphaOfRipple(curIndex0));
    curRadius0 = getRadiusOnRipple(curIndex0);
    curIndex0++;
    if (curIndex0 > (mRMaxRadius - mRMinRadius) / distance)
        curIndex0 = 0;

    cancleHandle(MSG_DRAW0);
复制代码

圆球动画效果在这里是每隔200ms在相应的位置进行绘制,由于波纹扩散周期较短,所以我将圆球的隔旋转周期定为了45度,可自行修改。这里的难点也是在于怎么找到圆球的圆心坐标,即根据圆心坐标,半径,扇形角度来求扇形终射线与圆弧交叉点的x, y坐标的问题,上文也已经说过了,代码见下:

    private PointF drawCircleOnRipple(int msg, int index) {

        // 周期开始,随机初始角度
        if (index == 0 && msg == MSG_DRAW0) {
            cirAngel0 = (float) (Math.random() * -360 + 180);
        } else if (index == 0) {
            cirAngel1 = (float) (Math.random() * -360 + 180);
        }
            
        return CommentUtil.calcArcEndPointXY(
                mRMaxRadius + getPaddingLeft() + mStrokeWidth
                    , mRMaxRadius + getPaddingTop() + mStrokeWidth
                    , msg == MSG_DRAW0 ? mCurRadius0 : mCurRadius1
                    // 每个周期旋转45度
                    , (msg == MSG_DRAW0 ? curIndex0 : curIndex1) * 1.0f 
                        / ((mRMaxRadius - mRMinRadius) / distance) * 45f
                    , msg == MSG_DRAW0 ? cirAngel0 : cirAngel1);
    }
复制代码

波纹和圆球的颜色渐变效果,由于不是渐变到全透明,所以我的alpha取值范围105-255,代码见下:

    private int getAlphaOfRipple(int curIndex) {
        final int alpha = curIndex * 150 * distance / (mRMaxRadius - mRMinRadius); // 只取150的二进制
        return 255 - alpha;
    }
复制代码

其余的细节和方法就不贴了,都是比较常规的Paint方法。

栗子ε

这个效果是由四段贝塞尔曲线来拟合实现的,见下:

2021-04-25 at 23.32.00.gif

通过贝塞尔曲线我们能做很多的效果,todo:后续我会再出一篇贝塞尔的小栗子。下面是一个三阶贝塞尔曲线的动态图及公式,它通过控制曲线上的四个点:起始点、终止点以及两个相互分离的控制点来创造、编辑图形。其中参数 t 的值等于线段上某一个点距离起点的长度除以该线段长度。

ここに画像の説明を挿入

ここに画像の説明を挿入 ここに画像の説明を挿入 ここに画像の説明を挿入

nセグメントの3次ベジェ曲線で円をフィッティングする場合、曲線の終点から最も近い制御点までの最適な距離は(4/3)tan(π/(2n))です。そして、t=0.5の点は弧上になければなりません。

222.png

したがって、4つのベジェ曲線で円をフィットさせたい場合は、hの値を簡単に導き出すことができます。

555.png

4つのベジェ曲線(h = 0.552284749831)を取り、それらを初期状態として完全な円に結合してみましょう。hのこの臨界値を見つけることの別の効果は、移動する必要のあるb曲線がすべて凸状であることです。開始点と制御点の参照コードは次のとおりです。

    private void calculateCp() {
        b = 0.552284749831;

        if (startP == null || endP == null) {
            startP = new PointF(0, - mRadius);
            endP = new PointF(mRadius, 0);
        }

        // 平移后的画布坐标,坐标(0,0)为圆心
        cp1 = new PointF((float) (mRadius * b), - mRadius);
        cp2 = new PointF(mRadius, - (float) (mRadius * b));
    }
复制代码

動作中のリング効果は、制御点の座標を連続的にランダムに変更し、開始点にオフセットを追加した結果です。図の効果のコードは次のとおりです。

    private void calculateDynamicCp() {
        b = Math.random() * 0.44 + 0.55;

        // 平移后的画布坐标,坐标(0,0)为圆心,8个控制点和4个起始点,顺时针(12点->3点->6点->9点)
        if (points != null && points.size() != 0)
            points.clear();

        points.add(new PointF((float) (Math.random() * - 20 + 10) , - mRadius - (float) (Math.random() * 20)));
        points.add(new PointF((float) (mRadius * b), - mRadius - (float) (Math.random() * 20)));
        points.add(new PointF(mRadius + (float) (Math.random() * 20), - mRadius - (float) (Math.random() * 10 + 10)));

        points.add(new PointF(mRadius + (float) (Math.random() * 10 + 10), (float) (Math.random() * - 20 + 10)));
        points.add(new PointF(mRadius + (float) (Math.random() * 20), (float) (Math.random() * 0.5 * mRadius * b + 0.5 *mRadius * b)));
        points.add(new PointF((float) (mRadius * b + 10), mRadius + (float) (Math.random() * 20)));

        points.add(new PointF((float) (Math.random() * - 20 + 10), mRadius + (float) (Math.random() * 20)));
        points.add(new PointF((float) (- mRadius * b), mRadius + (float) (Math.random() * 20)));
        points.add(new PointF(- mRadius - (float) (Math.random() * 20), (float) (mRadius * b)));

        points.add(new PointF(- mRadius - (float) (Math.random() * 10 + 10), (float) (Math.random() * - 20 + 10)));
        points.add(new PointF(- mRadius - (float) (Math.random() * 20), (float) (- mRadius * b)));
        points.add(new PointF((float) (- mRadius * b), - mRadius - (float) (Math.random() * 20)));
    }
复制代码

これが描画方法です。最初のリングの端点の座標は対称であるため、描画するにはキャンバスを回転させ続ける必要があります。これは非常に簡単です。動的リングには端点にオフセットがあるため、4つのベジェ曲線のみを順番に描画でき、各曲線はlineToで接続されます。参照コードは次のとおりです。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        canvas.save();
        canvas.translate(mRadius + mStrokeWidth + getPaddingLeft()
                , mRadius + mStrokeWidth + getPaddingTop());
        ... //first
        for(int index = 0; index < 4; index ++) {
            canvas.rotate(90f);
            bPath.moveTo(startP.x, startP.y);
            bPath.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, endP.x, endP.y);
            canvas.drawPath(bPath, bPaint);
            bPath.reset();
        }
        ...
        canvas.restore();
    }
复制代码

onDrawの場合、8つのコントロールポイントと4つの開始ポイントを設定します。キャンバスは回転しません。

    ...
    for (int index = 0; index < 4; index ++) {
        if (index == 0) {
           bPath.moveTo(points.get(0).x, points.get(0).y);
        } else {
            bPath.lineTo(points.get(index * 3).x, points.get(index * 3).y);
        }
                
        bPath.cubicTo(points.get(index * 3 + 1).x, points.get(index * 3 + 1).y
                , points.get(index * 3 + 2).x, points.get(index * 3 + 2).y
                , index != 3 ? points.get(index * 3 + 3).x : points.get(0).x
                , index != 3 ? points.get(index * 3 + 3).y : points.get(0).y);
        }

        canvas.drawPath(bPath, bPaint);
        bPath.reset();
    }
    ...
复制代码

残りの詳細と方法は掲載されません。それらはすべて従来のペイント方法です。

この記事はここにあります。この種の記事への反応が悪くなければ、フォローアップでいくつかの興味深い小さな栗を検討します。この記事があなたに役立つなら、それを好きにしてください、みんなの肯定は飲酒運転が書き続けるための原動力でもあります。

おすすめ

転載: juejin.im/post/6955130977836335117