一种用于显示放大倍数的自定义动画控件

能够显示1x~8x 范围的放大缩小动画,移动单位为每缩放0.1x的时候移动一格,因此共有80格,这个例子是使用触摸位置的x轴占整个x轴多少来控制倍数控件的展现。具体运算主要看initView方法便可。实际效果会比图片好,图片跳帧很严重,实际上每一格移动的时候都会有一个小的跳跃感动画,能够下载下来试试:java

代码文件和测试apkandroid

 

package com.cjz.zoombar;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.example.chenjiezhu.myapplication.R;

public class MainActivity extends Activity {

    private MzZoomCircleView zoomCircleView;
    private float prevZoomVal, currentZoomVal;
    private TextView btn_zoom_val;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.zoom_bar);
        zoomCircleView = findViewById(R.id.zoomCircleView);
        btn_zoom_val = findViewById(R.id.tv_zoom_val);
        zoomCircleView.setVisible(true);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean val = super.onTouchEvent(event);
        Log.i("cjztest", event.toString());
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                zoomCircleView.setCurrentValue((8f * event.getX() / 1080f), false);
                btn_zoom_val.setText((8f * event.getX() / 1080f) + "x");
                break;
            case MotionEvent.ACTION_UP:
                prevZoomVal = currentZoomVal;
                break;
        }
        return val;
    }
}

动画控件:canvas

package com.cjz.zoombar;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.PathInterpolator;


import java.util.ArrayList;
import java.util.List;


public class MzZoomCircleView extends View {

    private int mCircleRadius;
    private int mCircleStartPadding;
    private int mCirclePadding;
    private Paint mCirclePaint;
    private int mIndicatorRadius;
    private boolean mVisible;
    private int mWidth;
    private int mHeight;
    private static final float MIN_ZOOM_VALUE = 1.0f;
    /**
     * value range: [1.0f ~ 8.0f]
     */
    private float mMaxZoomValue = 8.0f;
    private float mPrevMaxZoomValue = Float.MIN_VALUE;
    private float mCurrentValue = 1.0f;
    private float mLastValue = 1.0f;
    private float mMoveX = 0;
    private float mIndicatorModeX;
    private ValueAnimator mValueAnimator;
    private ValueAnimator mAlphaAnimator;
    private boolean mIsClickZoomIndicator = false;

    private Rect mValidDrawBounds = new Rect();
    private List<Calibration> mShowCirclePoints = new ArrayList<>();
    private List<Calibration> mShowWithAlphaPoints = new ArrayList<>();

    /**
     * record the circle point and alpha
     */
    class Calibration {
        private float x;
        private float y;
        private float mAlpha;
        private int defaultX;

        public void setX(float x) {
            this.x = x;
        }

        public void setY(float y) {
            this.y = y;
        }

        public void setAlpha(float alpha) {
            mAlpha = alpha;
        }

        public void setDefaultX(int defaultX) {
            this.defaultX = defaultX;
        }
    }


    public MzZoomCircleView(Context context) {
        this(context, null);
    }

    public MzZoomCircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MzZoomCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initValue();
        initPaint();
    }

    private void initValue() {
        mCircleRadius = dip2px(getContext(), 1);
        mCircleStartPadding = dip2px(getContext(), 5);
        mCirclePadding = dip2px(getContext(), 6);
        mIndicatorRadius = dip2px(getContext(), 16);
//        mIndicatorRadius = 16;
    }

    private void initPaint() {
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        Typeface typeface = Typeface.create("SFDIN-medium", Typeface.NORMAL);
        mCirclePaint.setTypeface(typeface);
        mCirclePaint.setColor(Color.RED);
    }

    private void initView(float offset, boolean isTarget) {
        float positionOffset = offset;
        mShowWithAlphaPoints.clear();
        mValidDrawBounds.set(getPaddingStart() - 10, getPaddingTop(), mWidth - getPaddingEnd() + 10, mHeight - getPaddingBottom());
        int circleSize = 2 * mCircleRadius + mCirclePadding;
        //create points array
        if (mShowCirclePoints.isEmpty() || !floatEquals(mPrevMaxZoomValue, mMaxZoomValue)) {
            mShowCirclePoints.clear();
            for (int i = 0; i < (mMaxZoomValue - 1f) / 0.1f; i++) {
                Calibration calibration = new Calibration();
                calibration.setDefaultX(i * circleSize + mWidth / 2);  //mWidth / 2  右半轴为起始点,每一个加上空位的大小是(2 * mCircleRadius + mCirclePadding)
                calibration.setY(mHeight / 2);
                calibration.setAlpha(0);
                mShowCirclePoints.add(calibration);
            }
            mPrevMaxZoomValue = mMaxZoomValue;
        }
        //give them position
        for (int i = 0; i < mShowCirclePoints.size(); i++) {
            Calibration calibration = mShowCirclePoints.get(i);
            if (!isTarget) {
                calibration.x = calibration.defaultX - ((int) ((positionOffset - 1f) / (mMaxZoomValue) * mShowCirclePoints.size())) * circleSize;
            } else {
                calibration.x = calibration.defaultX + positionOffset;
            }
            calibration.y = mHeight / 2;
            calibration.setAlpha(0);
            //绕开中间的按钮,从中线开始左右靠
            if (calibration.x > mWidth / 2) {
                calibration.x += mIndicatorRadius;
            } else if (calibration.x < mWidth / 2) {
                calibration.x -= mIndicatorRadius;
            }
            //set alpha values
            if (calibration.x < (mWidth / 2 - (mIndicatorRadius + mCircleStartPadding))) {
                float startPoint = mWidth / 2 - circleSize * 10 - (mIndicatorRadius + mCircleStartPadding);
                if (calibration.x > startPoint) {
                    calibration.setAlpha((calibration.x - startPoint) / (mWidth / 2 - startPoint) * 255f);
                }
            } else if (calibration.x > (mWidth / 2 + (mIndicatorRadius + mCircleStartPadding))) {
                float endPoint = mWidth / 2 + circleSize * 10 + (mIndicatorRadius + mCircleStartPadding);
                if (calibration.x < endPoint) {
                    calibration.setAlpha((1 - (calibration.x - mWidth / 2) / (endPoint - mWidth / 2)) * 255f);
                }
            }
            mShowWithAlphaPoints.add(calibration);
        }
    }

    public void updateMaxZoomValue(float maxZoomValue) {
        mMaxZoomValue = maxZoomValue;
    }

    public float getCurrentValue() {
        return mCurrentValue;
    }

    public void setVisible(boolean visible) {
        mVisible = visible;
        this.setVisibility(mVisible ? VISIBLE : INVISIBLE);
    }

    public void resetCircleValue() {
        setCurrentValue(MIN_ZOOM_VALUE, false);
    }

    public void handleZoomCircleViewAnim(final boolean show, final boolean resetCircleValue) {
        if (mAlphaAnimator != null && mAlphaAnimator.isRunning()) {
            mAlphaAnimator.end();
        }
        if (show) {
            mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0.0f, 1.0f);
            mAlphaAnimator.setDuration(80);
        } else {
            mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1.0f, 0.0f);
            mAlphaAnimator.setDuration(100);
        }
        mAlphaAnimator.setInterpolator(new PathInterpolator(0.3f, 0f, 0.66f, 1f));
        mAlphaAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                if (show) {
                    setVisible(true);
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (!show) {
                    setVisible(false);
                }
                if (resetCircleValue) {
                    if (!floatEquals(MIN_ZOOM_VALUE, mCurrentValue)) {
                        mMoveX = (mCurrentValue - mLastValue) * 10 * (2 * mCircleRadius + mCirclePadding);
                    }
                    initView(mCurrentValue, false);
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });
        mAlphaAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!mVisible) {
            return;
        }
        mWidth = getWidth();
        mHeight = getHeight();
        for (Calibration calibration : mShowWithAlphaPoints) {
            mCirclePaint.setAlpha((int) calibration.mAlpha);
            canvas.drawCircle(calibration.x, calibration.y, mCircleRadius, mCirclePaint);
        }
        Paint indicatorPaint = new Paint();
        indicatorPaint.setColor(Color.BLACK);
        indicatorPaint.setStyle(Paint.Style.STROKE);
        indicatorPaint.setAntiAlias(true);
        indicatorPaint.setStrokeWidth(3f);
        indicatorPaint.setTextSize(dip2px(getContext(), 10));
        canvas.drawCircle(mWidth / 2f, mHeight / 2f, mIndicatorRadius, indicatorPaint);
        canvas.drawText(String.format("%.1fx", mCurrentValue), mWidth / 2 - dip2px(getContext(), 10) / 2 - dip2px(getContext(), 4),
                mHeight / 2 + dip2px(getContext(), 4), indicatorPaint);
        if (mValueAnimator != null && !mValueAnimator.isRunning() && mIsClickZoomIndicator) {
            handleZoomCircleViewAnim(false, true);
            mIsClickZoomIndicator = false;
        }
    }

    public synchronized void setCurrentValue(float value, boolean clickIndicator) {
        value = Float.parseFloat(String.format("%.1f", value));
        if (floatEquals(value, mCurrentValue)) {
            return;
        }
        Log.i("cjztest", "value:" + value);
        mIsClickZoomIndicator = clickIndicator;
        if (value < MIN_ZOOM_VALUE) {
            value = MIN_ZOOM_VALUE;
        } else if (value > mMaxZoomValue) {
            value = mMaxZoomValue;
        }
        if (mValueAnimator != null && mValueAnimator.isRunning()) {
            mValueAnimator.end();
        }
        if (floatEquals(mCurrentValue, value)) {
            return;
        }
        mLastValue = mCurrentValue;
        mCurrentValue = value;
        if (mIsClickZoomIndicator) {
            if (mCurrentValue > mLastValue) {
                mIndicatorModeX = mCirclePadding * 3 + mCircleRadius * 6;
            } else {
                mIndicatorModeX = -(mCirclePadding * 3 + mCircleRadius * 6);
                if (floatEquals(MIN_ZOOM_VALUE, mCurrentValue)) {
                    initView(mCurrentValue, false);
                }
            }
            scrollToTarget(mIndicatorModeX);
        } else {
            mMoveX = (mCurrentValue - mLastValue) * 10 * (2 * mCircleRadius + mCirclePadding);
            scrollToTarget(mMoveX);
        }
    }

    private void scrollToTarget(final float x) {
        final int circleSize = (2 * mCircleRadius + mCirclePadding);
        mValueAnimator = new ValueAnimator().ofFloat(0, x);
        if (mIsClickZoomIndicator) {
            mValueAnimator.setDuration(240);
        } else {
            mValueAnimator.setDuration(150);
        }
        mValueAnimator.setInterpolator(new DecelerateInterpolator());
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (mIsClickZoomIndicator) {
                    initView((float) animation.getAnimatedValue() - mWidth, true);  //loop anim
                } else {
                    //0.1f means a point, so is zoom value is 1, the left side have 1 / 0.1f = 10 points
                    int endX = Math.round((mCurrentValue - 1f) * 10) * circleSize;
                    int startX = Math.round((mLastValue - 1f) * 10) * circleSize;
                    float targetVal = -(startX + (endX - startX) * (Math.abs((float) animation.getAnimatedValue() / x)));
                    initView(Math.round(targetVal), true);
                }
                invalidate();
            }
        });
        mValueAnimator.start();
    }

    private static boolean floatEquals(float a, float b){
        if(Math.abs(a - b) <= 0.00001f){
            return true;
        } else {
            return false;
        }
    }

    private static boolean floatEquals(double a, double b){
        if(Math.abs(a - b) <= 0.0000001f){
            return true;
        } else {
            return false;
        }
    }

    private int dip2px(Context context, float dipValue) {
        float m = context.getResources().getDisplayMetrics().density ;
        return (int) (dipValue * m) ;
    }
}