自定义FM刻度尺

自定义FM刻度尺

Scroller学习

  1. Scroller:Android里面专门处理滚动的工具类,比如ViewPager、ListView等。
  2. scrollTo:设置View的滚动位置,并且会刷新View。
  3. scrollBy:移动View的滚动位置,并且会刷新View。
  4. 无论scrollTo还是scrollBy都是移动父布局里的内容,但是scrollTo再调用后再次调用就没有效果了,这是因为scrollTo()方法是让View相对于初始的位置滚动某段距离;而scrollBy则可以继续调用,这是因为scrollBy()方法是让View相对于当前的位置滚动某段距离;无论是scrollTo还是scrollBy最终都会调用invalidate方法进行刷新。

自定义View中的onMeasure方法

	 /** 
         * 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式 
         */  
	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    //MeasureSpec值由specMode和specSize共同组成,onMeasure两个参数的作用根据specMode的不同,有所区别。
    //当specMode为EXACTLY时,子视图的大小会根据specSize的大小来设置,对于布局参数中的match_parent或者精确大小值
    //当specMode为AT_MOST时,这两个参数只表示了子视图当前可以使用的最大空间大小,而子视图的实际大小不一定是specSize。所以我们自定义View时,重写onMeasure方法主要是在AT_MOST模式时,为子视图设置一个默认的大小,对于布局参数wrap_content。
    if (heightSpecMode == MeasureSpec.AT_MOST) {
        //这个方法确定了当前View的大小
        setMeasuredDimension(widthSpecSize, SystemUtil.dp2px(getContext(),60));
    } else {
        setMeasuredDimension(widthSpecSize, heightSpecSize);
    }

Measure.Mode测量模式:

  1. UNSPECIFIED:父容器不对子View做任何限制,要多大给多大,一般用于系统内部,这里就不用多考虑
  2. EXACTLY:精准模式,一般View指定了具体的大小(dp/px)或者设置match_parent
  3. AT_MOST:父容器制定了一个可用的大小,子View不能大于这个值,这个是在布局设置wrap_content

文字属性

请添加图片描述

图中有五条线结合官方文档,自上而下来解释:

  1. top:给定文本大小下,字体中最高字符高于基线之上的最大距离
  2. ascent:单个文本下超出基线之上的推荐距离
  3. baseLine:文本基线
  4. descent:单个字符超出基线之上的推荐距离
  5. bottom:字体中最低字符超出基线之下的最大距离
  6. leading:文本行与文本行之间的距离
 //得到文字的字体属性和测量
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//文字设置在View的中间
float y = height / 2 + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) / 2;

文本字体的高度可以用Math.abs(ascent) + descent,那么文本高度的一半也就是**(Math.abs(ascent) + descent)/ 2**。因此最终红色原点对于整个View的y坐标是float y = height / 2 + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) / 2;

完成滑动

  1. 首先创建滚动实例

        //创建滑动实例
            mScroller = new Scroller(context);
    
  2. Android中View的坐标和获取距离方法请添加图片描述View提供的获取坐标以及距离的方法:

    1. getTop获取的是View自身顶边到父布局顶边的距离
    2. getLeft获取的是View自身左边到父布局左边的距离
    3. getRight获取的是View自身右边到父布局左边的距离
    4. getBottom获取的是View自身底边到父布局顶边的距离

    MotionEvent提供的方法:

     1.   getX获取触摸事件触摸点距离控件左边的距离,是视图坐标
     2.   getY获取触摸事件触摸点距离控件顶边的距离,是视图坐标
     3.   getRawX获取触摸事件触摸点距离整个屏幕左边的距离,是绝对坐标
     4.   getRawY获取触摸事件触摸点距离整个屏幕顶边的距离,是绝对坐标
    
  3. 获取View自身左右边距离到到父布局左右距离

     //得到左右边界
    leftBorder = getLeft();
    rightBorder = (int)paint.measureText(currentNum);
    
  4. 左右边界检测

      //如果右滑时 内容左边界超过初始化时候的左边界 就还是初始化时候的状态
    if (getScrollX() + scrolledX < mLeftBorder - getWidth() / 2) {
                scrollTo((int) (-getWidth() / 2 + mLeftBorder), 0);
            } else if (getScrollX() + scrolledX > mRightBorder) {
        //如果左滑  这里判断右边界
                scrollTo((int) mRightBorder, 0);
            } else {
        //在左右边界中 自由滑动
                scrollBy(scrolledX, 0);
            }
    

    getScrollX方法:返回当前View视图左上角X坐标与View视图初始位置左上角X坐标的距离,注意,这是以屏幕坐标为参照点,View右移这个值由正变为负数一直递增。请添加图片描述

  5. 完整代码,增加惯性滑动和手势监听:

    public class RulerView extends View {
    
        private static final String TAG = "SlideView";
    
        //刻度线画笔
        private Paint mLinePaint;
        //数字画笔
        private Paint mTextPaint;
        //指针画笔
        private Paint mPointPaint;
    
        //当前刻度值
        private String mCurrentNum;
    
        //左边界
        private float mLeftBorder;
        //右边界
        private float mRightBorder;
    
        //手指最开始触摸屏幕的X坐标
        private float mDownX;
        //上次触发移动事件的坐标
        private float mLastMoveX;
        //当前手指移动结束后停下来的X坐标
        private float mCurrentMoveX;
    
        //10的倍数高度
        private float mLongDegreeLine;
        //除以10余5高度
        private float mMiddleDegreeLine;
        //小刻度
        private float mMinDegreeLine;
    
        //滑动实例
        private Scroller mScroller;
    
        //刻度间隔
        private float mLineDegreeSpace;
        //刻度颜色
        private int mLineDegreeColor;
        //刻度顶部线
        private float mTopDegreeLine;
        //中刻度数
        private int mLineCount;
        //最小滑动距离
        private int mTouchMinDistance;
    
    
        //数字颜色
        private int mTextColor;
        //数字大小
        private float mTextSize;
        //指针大小
        private float mPointWidth;
        //指针X轴位置
        private float mPointX;
        //格式化当前FM
        private DecimalFormat mFormat;
    
        //当前刻度值的大小
        private float mCurrentNumberSize;
        //当前刻度值的颜色
        private int mCurrentNumberColor;
    
        //监听手势滑动速度
        private VelocityTracker mVelocityTracker;
        //设置滑动惯性的最大速度
        private int mMaxVelocity;
        //设置滑动惯性的最大速度
        private int mMinVelocity;
    
        //手动设置刻度
        private float mSetDegree;
    
    
        public RulerView(Context context) {
            super(context);
            init(context);
        }
    
        public RulerView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        private void init(Context context) {
            mLinePaint = new Paint();
            mLinePaint.setAntiAlias(true);
            mLinePaint.setStyle(Paint.Style.STROKE);
            mLinePaint.setStrokeWidth(3);
    
            mTextPaint = new Paint();
            mTextPaint.setAntiAlias(true);
            mTextPaint.setStyle(Paint.Style.FILL);
            mTextPaint.setStrokeWidth(5);
    
            mPointPaint = new Paint();
            mPointPaint.setAntiAlias(true);
            mPointPaint.setStyle(Paint.Style.FILL);
            mPointWidth = dp2px(4, context);
            mPointPaint.setStrokeWidth(mPointWidth);
            mPointPaint.setColor(0xFF4FBA75);
    
            mFormat = new DecimalFormat("0.0");
    
            mScroller = new Scroller(context);
    
            mLineDegreeColor = Color.GRAY;
            mTopDegreeLine = dp2px(45, context);
            mLineDegreeSpace = dp2px(10, context);
    
            mLineCount = 41;
    
            ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
            mTouchMinDistance = viewConfiguration.getScaledTouchSlop();
    
            mLongDegreeLine = dp2px(35, context);
            mMiddleDegreeLine = dp2px(20, context);
            mMinDegreeLine = dp2px(10, context);
    
            mRightBorder = mLineDegreeSpace * mLineCount * 5;
    
            mTextColor = Color.BLACK;
            mTextSize = dp2px(15, context);
    
    
            mCurrentNumberColor = 0xFF4FBA75;
            mCurrentNumberSize = dp2px(30, context);
            //初始化实例
            mVelocityTracker = VelocityTracker.obtain();
            //设置最大速度和最小速度
            mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
            mMinVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        }
    
        private static int dp2px(float dp, Context context) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            Log.e(TAG, "onMeasure: widthSpecMode = " + widthSpecMode);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            Log.e(TAG, "onMeasure: widthSpecSize = " + widthSpecSize);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            Log.e(TAG, "onMeasure: heightSpecMode = " + heightSpecMode);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
            Log.e(TAG, "onMeasure: heightSpecSize = " + heightSpecSize);
            if (heightSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSpecSize, dp2px(120, getContext()));
            } else {
                setMeasuredDimension(widthSpecSize, heightSpecSize);
            }
            //使得绘制时从中间开始绘制
            mLeftBorder = getMeasuredWidth() / 2;
            mPointX = getMeasuredWidth() / 2;
            Log.e(TAG, "onMeasure: mPointX = " + mPointX);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            //在触摸事件onTouchEvent初始化mVelocityTracker
            mVelocityTracker.addMovement(event);
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //滑动中点击,停止滑动
                    if (!mScroller.isFinished()) {
                        mScroller.abortAnimation();
                    }
                    //记录初始触摸屏幕下的坐标
                    mDownX = event.getRawX();
                    mLastMoveX = mDownX;
                    break;
                case MotionEvent.ACTION_MOVE:
                    mCurrentMoveX = event.getRawX();
                    //本次的滑动距离
                    int scrolledX = (int) (mLastMoveX - mCurrentMoveX);
                    Log.e(TAG, "onTouchEvent: scrolledX = " + scrolledX);
                    scrollBy(scrolledX, 0);
                    mLastMoveX = mCurrentMoveX;
                    break;
                case MotionEvent.ACTION_UP:
                    //计算松手后当前滑动的速率,一秒内滑动多少像素
                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
                    //获取横向滑动速度
                    int xVelocity = (int) mVelocityTracker.getXVelocity();
                    //判断滑动速度是否大于最小滑动速度
                    if (Math.abs(xVelocity) > mMinVelocity) {
                        mScroller.fling(getScrollX(), 0, -xVelocity,
                                0, (int) -mRightBorder, (int) mRightBorder, 0, 0);
                        Log.e(TAG, "setDegree: getScroller = " + getScrollX());
                    } else {
                        Log.d(TAG, "onTouchEvent: xVelocity < mMinVelocity");
                    }
                    moveToRecentlyDegree();
                    break;
                case MotionEvent.ACTION_CANCEL:
                    //VelocityTracker资源回收
                    if (mVelocityTracker != null) {
                        mVelocityTracker.recycle();
                        mVelocityTracker = null;
                    } else {
                        Log.d(TAG, "onTouchEvent: mVelocityTracker is null");
                    }
                    break;
                default:
                    break;
            }
            return true;
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                //最后一次滑动则进行边界检测
                if (!mScroller.computeScrollOffset()) {
                    moveToRecentlyDegree();
                }
            }
        }
    
        @Override
        public void scrollTo(int x, int y) {
            //左边界检测
            if (x < mLeftBorder - getWidth() / 2) {
                x = (int) (-getWidth() / 2 + mLeftBorder);
            } else if (x > mRightBorder) {
                //右边界检测
                x = (int) mRightBorder;
            }
            if (x != getScrollX()) {
                super.scrollTo(x, y);
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //确定顶部长线的左端
            float x = mLeftBorder;
            //确定顶部长线
            float y = mTopDegreeLine;
    
            mLinePaint.setColor(mLineDegreeColor);
            mLinePaint.setStrokeWidth(3);
            //绘制顶部长线
            //canvas.drawLine(x, y, mRightBorder - mRightSpace - mLeftSpace + getWidth() / 2, y, mLinePaint);
            canvas.drawLine(x, y, mRightBorder + getWidth() / 2, y, mLinePaint);
            for (int i = 0; i <= mLineCount * 5; i++) {
                if (i % 10 == 0) {
                    mLinePaint.setStrokeWidth(5);
                    canvas.drawLine(x, y, x, y + mLongDegreeLine, mLinePaint);
                    //FM范围为87.5-108
                    String num = String.valueOf(i / 10 + 87.5);
                    //测量数字宽度
                    float textWidth = mTextPaint.measureText(num);
                    mTextPaint.setColor(mTextColor);
                    mTextPaint.setTextSize(mTextSize);
                    canvas.drawText(num, x - textWidth / 2, y + mLongDegreeLine + dp2px(25, getContext()), mTextPaint);
                } else if (i % 10 == 5) {
                    mLinePaint.setStrokeWidth(5);
                    canvas.drawLine(x, y, x, y + mMiddleDegreeLine, mLinePaint);
                } else {
                    mLinePaint.setStrokeWidth(3);
                    canvas.drawLine(x, y, x, y + mMinDegreeLine, mLinePaint);
                }
                x += mLineDegreeSpace;
            }
            mTextPaint.setColor(mCurrentNumberColor);
            mTextPaint.setTextSize(mCurrentNumberSize);
            mCurrentNum = mFormat.format((mPointX + getScrollX() - mLeftBorder) / (mLineDegreeSpace * 10.0f) + 87.5);
            float textWidth = mTextPaint.measureText(mCurrentNum);
            //绘制当前刻度数字
            canvas.drawText(mCurrentNum, mPointX - textWidth / 2 + getScrollX(),
                    y - dp2px(15, getContext()), mTextPaint);
            //绘制指针
            canvas.drawLine(mPointX + getScrollX(), y, mPointX + getScrollX(),
                    y + mLongDegreeLine + dp2px(3, getContext()), mPointPaint);
            Log.e(TAG, "onDraw: ");
        }
    
        /**
         * 吸附处理
         */
        private void moveToRecentlyDegree() {
            //获取当前刻度距离中心刻度的距离
            float distance = (mPointX + getScrollX() - mLeftBorder) % mLineDegreeSpace;
            //如果大于间隔的二分之一就向前滑
            if (distance >= mLineDegreeSpace / 2) {
                scrollBy((int) (mLineDegreeSpace - distance), 0);
            } else {
                //否则向后滑动
                scrollBy((int) -distance, 0);
            }
        }
    
        //自定义设置刻度
        public void setDegree(float degree) {
            Log.e(TAG, "setDegree: getScroller = " + getScrollX());
            mSetDegree = degree;
            int scrolledX = (int) ((degree - Float.parseFloat(mCurrentNum)) * (mLineDegreeSpace * 10.0f));
            scrollBy(scrolledX, 0);
            moveToRecentlyDegree();
            mSetDegree = 0;
        }
    }
    

    MainActivity:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            RulerView rulerView = findViewById(R.id.ruler_view);
            EditText editText = findViewById(R.id.frequency);
            editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
            Button send = findViewById(R.id.send);
    
            send.setOnClickListener(view -> {
                String frequency;
                float fr;
                if (!TextUtils.isEmpty(editText.getText().toString())){
                    frequency = editText.getText().toString();
                    if (frequency.endsWith(".")){
                        Toast.makeText(MainActivity.this,"请输入正确的电台!",Toast.LENGTH_SHORT).show();
                        return;
                    }
                    fr = Float.parseFloat(frequency);
                    if (fr < 87.5 || fr > 108 ){
                        Toast.makeText(MainActivity.this,"请输入电台FM范围内的电台频点!",Toast.LENGTH_SHORT).show();
                    } else {
                        rulerView.setDegree(fr);
                    }
                } else {
                    Log.d("TAG", "onCreate: editText is null");
                }
            });
        }
    }
    

    布局文件:

    <?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">
    
        <com.example.scorllerdemo.RulerView
            android:id="@+id/ruler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <EditText
            android:id="@+id/frequency"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/frequency" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

立花泷える宫水三叶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值