手把手教你用自定义View实现 长按出现水漫效果的进度按钮(长按取消)

前言:这是自己写的练手Demo,以后会写一个进阶版的水漫效果按钮,长按出现波浪形状的水波,更好看,可玩性更高,感兴趣的可以关注下后续。

效果展示

效果图

实现方法

一、思路

通过自定义View画出一个长按出现水漫效果进度条的按钮,当进度条满了进行接口回调,告诉当前运行的Activity,动画执行完毕。

1.画出中心带圆角的长方形按钮。

  • 确定自定义View的大小尺寸。
  • 画出中心图片。

2.确定水漫进度条的大小,先将进度条的形状、颜色、大小提前设置好。

3.监听点击事件,利用事件分发机制,当手指点击按钮时,将提前设置好的水漫进度条以一部分的形式显示出来,覆盖在原有的背景上,用逐渐增加和递减的数值控制水漫进度条占总体的占比。

  • 获取焦点时,画出水漫进度条,使用定时器,定时增加显示进度的占比数值。
  • 失去焦点时,控制水漫进度条的数值逐渐减少。

二、代码实现

自定义View核心代码:
1.属性的初始化和接口

	private void getAttrValue(AttributeSet attrs) {
    
    
        TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView);
        mBgColor = ta.getColor(R.styleable.WaterProgressView_background_color, Color.parseColor("#EE191C"));
        mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color, Color.parseColor("#FF5263"));
        ta.recycle();
    }

    private void init() {
    
    
        //初始化画带圆角矩形的画笔
        mBgPaint = new Paint();
        mBgPaint.setColor(mBgColor);
        mBgPaint.setStyle(Paint.Style.FILL);
        mBgPaint.setAntiAlias(true);
        mBgPaint.setDither(true);

        //初始化画进度的paint
        mProgressPaint = new Paint();
        mProgressPaint.setColor(mProgressColor);
        mProgressPaint.setAntiAlias(true);
        mProgressPaint.setDither(true);
        mProgressPaint.setStyle(Paint.Style.FILL);

        //初始化画完整进度的paint
        mProgressBgPaint = new Paint();
        mProgressBgPaint.setColor(mProgressColor);
        mProgressBgPaint.setAntiAlias(true);
        mProgressBgPaint.setDither(true);
        mProgressBgPaint.setStyle(Paint.Style.FILL);

        //显示背景的矩形范围
        mRectangleRectF = new RectF();
        //显示进度的矩形范围
        mProgressRectangleRectF = new RectF();

        mHandler = new Handler();
        //环形进度条自动增加逻辑
        mRunnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                mProgress += 1;
                setProgress(mProgress);
                //更新进度的接口回调
                if (mOnLongClickStateListener != null){
    
    
                    mOnLongClickStateListener.onProgress(mProgress);
                }
                if (mProgress < mTargetProgress){
    
    
                    mHandler.postDelayed(this, 1);
                }else {
    
    
                    //当环形进度条达到100,取消循环,进度置零,调用接口的完成回调
                    mProgress = 0;
                    if (mOnLongClickStateListener != null){
    
    
                        mOnLongClickStateListener.onFinish();
                    }
                }
            }
        };

        //取消动作的逻辑
        mCancelRunnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                setProgress(mProgress);
                //当进度为0时,取消循环
                if (mProgress <= 0){
    
    
                    return;
                }else if (mProgress < 10){
    
    
                    //当进度降低到较低状态时,减缓降低的速度,每次减2
                    mProgress -= 2;
                }else {
    
    
                    //进度较高时,进度条减少的速度加快,每次减7
                    mProgress -= 7;
                }

                if (mProgress > 0){
    
    
                    mHandler.postDelayed(this, FINISH_TIME / 100);
                }else {
    
    
                    //当环形进度条达到0,再次手动置零,调用接口的取消回调,并返回进度回调参数
                    mProgress = 0;
                    if (mOnLongClickStateListener != null){
    
    
                        mOnLongClickStateListener.onCancel();
                    }
                }
                //更新进度的接口回调
                if (mOnLongClickStateListener != null){
    
    
                    mOnLongClickStateListener.onProgress(mProgress);
                }
            }
        };
    }
    
    /**
     * 长按完成和取消的接口
     */
    public interface OnLongClickStateListener {
    
    
        void onFinish();
        void onProgress(float progress);
        void onCancel();
    }

2.重写onTouchEvent(),利用事件分发,刷新按钮的状态

	    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
    
    
//        Log.e("TAG", "onTouchEvent: " + mProgress );
        switch (motionEvent.getAction()) {
    
    
            case MotionEvent.ACTION_DOWN:
                //当手指按下时,执行环形进度条增加Runnable,进度条开始增加
                mHandler.post(mRunnable);
                mHandler.removeCallbacks(mCancelRunnable);
                Log.e("TAG", "onTouchEvent: ACTION_DOWN");
                return true;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //当手指松开时,执行环形进度条减少Runnable,进度条开始减少
                mHandler.removeCallbacks(mRunnable);
                mHandler.post(mCancelRunnable);
                Log.e("TAG", "onTouchEvent: ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                break;
        }
        return false;
    }

3.重写onDraw方法和onSizeChange方法
每次调用invalidate()方法都会进行一次onDraw。

	@Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        //画背景圆角矩形
        canvas.drawRoundRect(mRectangleRectF, mViewWidth / 2, mViewWidth / 2, mBgPaint);
        //进度圆角矩形,只画出完整进度的(mProgress / mTargetProgress)部分,主需要控制改画出的部分的四个顶点坐标,即可单独画出顶点内的部分
        canvas.drawRect(0, mViewHeight * (1 - mProgress / mTargetProgress), mViewWidth, mViewHeight, mProgressPaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    
    
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        //背景的矩形
        mRectangleRectF.set(0, 0, mViewWidth, mViewHeight);
        //进度的矩形,因为防止计算数值时精度丢失,导致进度条面积小于实际背景面积,当水漫进度增加时,无法完全覆盖背景,所以范围增加1px
        mProgressRectangleRectF.set(-1, - 1, mViewWidth + 1, mViewHeight + 1);
        dealBitmap();
    }

4.处理bitmap方法
将完整的水漫进度条画到bitmap里,通过mProgressPaint.setShader()方法,将完整的bitmap设置到画笔里,那么这个歌画笔就可以带有完整的水漫进度,我们只要使用这个画笔,只显示我们需要画的那一部分就好。

    //处理Bitmap
    private void dealBitmap(){
    
    
        mBitmap = Bitmap.createBitmap(mViewWidth,
                mViewHeight , Bitmap.Config.ARGB_8888);
        //把完整进度画进bitmap
        Canvas canvas = new Canvas(mBitmap);
        canvas.drawRoundRect(mProgressRectangleRectF, mViewWidth / 2, mViewWidth / 2, mProgressBgPaint);
        mProgressPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    }

三、完整代码及使用示例

1.自定义View完整代码

public class WaterProgressView extends View {
    
    


    private Context mContext;
    private Paint mBgPaint; // 背景的画笔
    private Paint mProgressBgPaint; // 完整进度背景的画笔
    private Paint mProgressPaint; // 进度的画笔
    private RectF mRectangleRectF;  //背景范围矩形
    private RectF mProgressRectangleRectF;  //进度(水漫)范围矩形
    private int mViewWidth;//当前View的宽度
    private int mViewHeight;//当前View的高度
    private int mBgColor;  //背景颜色
    private float mProgress;  //进度
    private int mTargetProgress = 100;  //最大进度
    private int mProgressColor;  //进度颜色

    private Bitmap mBitmap;  //完整进度的bitmap对象

    private Handler mHandler;
    private Runnable mRunnable;  //长按动作的计时器
    private Runnable mCancelRunnable;  //取消动作的计时器
    private static final int FINISH_TIME = 1000;

    private WaterProgressView.OnLongClickStateListener mOnLongClickStateListener;


    public void setOnLongClickStateListener(WaterProgressView.OnLongClickStateListener onLongClickStateListener) {
    
    
        this.mOnLongClickStateListener = onLongClickStateListener;
    }


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

    public WaterProgressView(Context context, @Nullable AttributeSet attrs) {
    
    
        this(context, attrs,0);
    }

    public WaterProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
    
        super(context, attrs, defStyleAttr);
        mContext = context;
        getAttrValue(attrs);
        init();
    }

    private void getAttrValue(AttributeSet attrs) {
    
    
        TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView);
        mBgColor = ta.getColor(R.styleable.WaterProgressView_background_color, Color.parseColor("#EE191C"));
        mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color, Color.parseColor("#FF5263"));
        ta.recycle();
    }

    private void init() {
    
    
        //初始化画带圆角矩形的画笔
        mBgPaint = new Paint();
        mBgPaint.setColor(mBgColor);
        mBgPaint.setStyle(Paint.Style.FILL);
        mBgPaint.setAntiAlias(true);
        mBgPaint.setDither(true);

        //初始化画进度的paint
        mProgressPaint = new Paint();
        mProgressPaint.setColor(mProgressColor);
        mProgressPaint.setAntiAlias(true);
        mProgressPaint.setDither(true);
        mProgressPaint.setStyle(Paint.Style.FILL);

        //初始化画完整进度的paint
        mProgressBgPaint = new Paint();
        mProgressBgPaint.setColor(mProgressColor);
        mProgressBgPaint.setAntiAlias(true);
        mProgressBgPaint.setDither(true);
        mProgressBgPaint.setStyle(Paint.Style.FILL);

        //显示背景的矩形范围
        mRectangleRectF = new RectF();
        //显示进度的矩形范围
        mProgressRectangleRectF = new RectF();

        mHandler = new Handler();
        //环形进度条自动增加逻辑
        mRunnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                mProgress += 1;
                setProgress(mProgress);
                //更新进度的接口回调
                if (mOnLongClickStateListener != null){
    
    
                    mOnLongClickStateListener.onProgress(mProgress);
                }
                if (mProgress < mTargetProgress){
    
    
                    mHandler.postDelayed(this, 1);
                }else {
    
    
                    //当环形进度条达到100,取消循环,进度置零,调用接口的完成回调
                    mProgress = 0;
                    if (mOnLongClickStateListener != null){
    
    
                        mOnLongClickStateListener.onFinish();
                    }
                }
            }
        };

        //取消动作的逻辑
        mCancelRunnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                setProgress(mProgress);
                //当进度为0时,取消循环
                if (mProgress <= 0){
    
    
                    return;
                }else if (mProgress < 10){
    
    
                    //当进度降低到较低状态时,减缓降低的速度,每次减2
                    mProgress -= 2;
                }else {
    
    
                    //进度较高时,进度条减少的速度加快,每次减7
                    mProgress -= 7;
                }

                if (mProgress > 0){
    
    
                    mHandler.postDelayed(this, FINISH_TIME / 100);
                }else {
    
    
                    //当环形进度条达到0,再次手动置零,调用接口的取消回调,并返回进度回调参数
                    mProgress = 0;
                    if (mOnLongClickStateListener != null){
    
    
                        mOnLongClickStateListener.onCancel();
                    }
                }
                //更新进度的接口回调
                if (mOnLongClickStateListener != null){
    
    
                    mOnLongClickStateListener.onProgress(mProgress);
                }
            }
        };
    }

    @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        //画背景圆角矩形
        canvas.drawRoundRect(mRectangleRectF, mViewWidth / 2, mViewWidth / 2, mBgPaint);
        //进度圆角矩形,只画出完整进度的(mProgress / mTargetProgress)部分,主需要控制改画出的部分的四个顶点坐标,即可单独画出顶点内的部分
        canvas.drawRect(0, mViewHeight * (1 - mProgress / mTargetProgress), mViewWidth, mViewHeight, mProgressPaint);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    
    
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        //背景的矩形
        mRectangleRectF.set(0, 0, mViewWidth, mViewHeight);
        //进度的矩形,因为防止计算数值时精度丢失,导致进度条面积小于实际背景面积,当水漫进度增加时,无法完全覆盖背景,所以范围增加1px
        mProgressRectangleRectF.set(-1, - 1, mViewWidth + 1, mViewHeight + 1);
        dealBitmap();
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
    
    
//        Log.e("TAG", "onTouchEvent: " + mProgress );
        switch (motionEvent.getAction()) {
    
    
            case MotionEvent.ACTION_DOWN:
                //当手指按下时,执行环形进度条增加Runnable,进度条开始增加
                mHandler.post(mRunnable);
                mHandler.removeCallbacks(mCancelRunnable);
                Log.e("TAG", "onTouchEvent: ACTION_DOWN");
                return true;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //当手指松开时,执行环形进度条减少Runnable,进度条开始减少
                mHandler.removeCallbacks(mRunnable);
                mHandler.post(mCancelRunnable);
                Log.e("TAG", "onTouchEvent: ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                break;
        }
        return false;
    }

    //处理Bitmap
    private void dealBitmap(){
    
    
        mBitmap = Bitmap.createBitmap(mViewWidth,
                mViewHeight , Bitmap.Config.ARGB_8888);
        //把完整进度画进bitmap
        Canvas canvas = new Canvas(mBitmap);
        canvas.drawRoundRect(mProgressRectangleRectF, mViewWidth / 2, mViewWidth / 2, mProgressBgPaint);
        mProgressPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    }

    /**
     * 设置背景颜色
     * @param
     */
    public void setBgColor(int bgColor) {
    
    
        this.mBgColor = bgColor;
        mBgPaint.setColor(mBgColor);
        invalidate();
    }

    /**
     * 设置进度颜色
     * @param progressColor 进度颜色
     */
    public void setProgressColor(int progressColor) {
    
    
        this.mProgressColor = progressColor;
        //设置进度未满时的画笔颜色
        mProgressPaint.setColor(mProgressColor);
        //设置进度满时完整的画笔颜色
        mProgressBgPaint.setColor(mProgressColor);
        invalidate();
    }

    /**
     * 设置进度
     * @param mProgress(1-100f)
     */
    public void setProgress(float mProgress) {
    
    
        if(mProgress < 0){
    
    
            mProgress = 0;
        } else if(mProgress > 100){
    
    
            mProgress = 100;
        }
        this.mProgress = mProgress;
        invalidate();
    }

    /**
     * 长按完成和取消的接口
     */
    public interface OnLongClickStateListener {
    
    
        void onFinish();
        void onProgress(float progress);
        void onCancel();
    }
}

2.在style文件中,定义attr属性,没有则新建一个
attr.xml的位置
attr.xml的内容:

    <declare-styleable name="LongClickProgressView">
        <attr name="centerDrawable" format="reference"/>
    </declare-styleable>

3.在xml进行引用

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 暂时当前进度的TextView -->
    <TextView
        android:id="@+id/tv_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-medium"
        android:gravity="center"
        android:text="0%"
        android:textColor="#000000"
        android:textSize="39dp"
        android:layout_marginTop="60dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.longclickprogresswaterview.WaterProgressView
        android:id="@+id/btn_long_click_finish"
        android:layout_width="96dp"
        android:layout_height="48dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="@id/btn_long_click_finish"
        app:layout_constraintEnd_toEndOf="@id/btn_long_click_finish"
        app:layout_constraintTop_toTopOf="@id/btn_long_click_finish"
        app:layout_constraintBottom_toBottomOf="@id/btn_long_click_finish"
        android:fontFamily="sans-serif-medium"
        android:gravity="center"
        android:text="Finish"
        android:textAllCaps="true"
        android:textSize="12sp"
        android:textColor="#ffffff" />

</androidx.constraintlayout.widget.ConstraintLayout>

4.在Activity中对自定义View进行修改

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView textView = findViewById(R.id.tv_progress);

        WaterProgressView longClickProgressView = findViewById(R.id.btn_long_click_finish);
        longClickProgressView.setBgColor(Color.parseColor("#000000"));  //设置背景颜色
        longClickProgressView.setProgressColor(Color.parseColor("#FFFFFF"));  //设置进度(水漫)颜色
        //设置进度监听回调
        longClickProgressView.setOnLongClickStateListener(new WaterProgressView.OnLongClickStateListener() {
    
    
            @Override
            public void onFinish() {
    
    
                //当完成读条时执行
                Toast.makeText(MainActivity.this, "Finish!", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onProgress(float progress) {
    
    
                //进度条改变时执行
                textView.setText(progress + "%");
            }

            @Override
            public void onCancel() {
    
    
                //取消长按时执行
                Toast.makeText(MainActivity.this, "Cancel!", Toast.LENGTH_SHORT).show();
            }
        });
    }

}

Demo示例

Demo示例图

2.Demo地址
Github链接,欢迎Star和给出宝贵意见
https://github.com/Dengyaohui/LongClickProgressWaterViewDemo

CSDN下载地址
https://download.csdn.net/download/Nobody_else_/15184398

结语

后面会写一个进阶版的水漫效果按钮,长按出现波浪形状的水波,更加好看,可玩性更高。
更多其他的自定义View,可以看我的其他博客,欢迎批评指正。

仿Keep长按出现进度条的按钮
https://blog.csdn.net/Nobody_else_/article/details/113186425

共勉!

猜你喜欢

转载自blog.csdn.net/Nobody_else_/article/details/113784362
今日推荐