Android 自定义滚动选择控件开发

为何要从头开发滚动组件,为了能够让自己更加清楚和理解拖动效果是如何实现的,投掷效果是如何实现的。

 

我自己完成一个滚动选择控件,能够拖动选择,并且可以手指进行投掷操作(fling),手指离开,他也会继续滚动一段距离。因为做的是滚动选择,所以需要每次回自动滚动到恰好的位置。效果图如下

 

 

1.实现基本绘画

首先我需要绘画出一个类似LinearLayout里排放很多个相同的TextView的效果

但是因为选择控件固定显示几个item,所以我通过canvas.clipRect限制显示的范围

       canvas.clipRect(0, 0, 300, (int) (3.5 * interval + textSize * 3));

然后需要根据list的数据绘画相应的item,这里要注意每个item的高度等于字的高度textSize加上间距interval,然后drawText的startY是从字的左下角开始的,而且通过不断累积start来得出下一个item的高度

        for (String s : dataList) {

            Rect rect = new Rect();
            paint.getTextBounds(s, 0, s.length(), rect);
            // 获取到的是实际文字宽度
            int textWidth = rect.width();

            start = start + interval;
            canvas.drawText(s, (width - textWidth) / 2, start + textSize, paint);
            start = start + textSize;

        }

然后我们需要在显示的三个item的中间,绘画一个矩形,表示选中的item,画一个矩形需要四条线

        canvas.drawLine(linePad,
                (float) (interval * 1.5 + textSize), width - linePad, (float) (interval * 1.5 + textSize), zPaint);

        canvas.drawLine(linePad, (float) (interval * 2.5 + textSize * 2),
                width - linePad, (float) (interval * 2.5 + textSize * 2), zPaint);

        canvas.drawLine(linePad,
                (float) (interval * 1.5 + textSize), linePad, (float) (interval * 2.5 + textSize * 2), zPaint);

        canvas.drawLine(width - linePad, (float) (interval * 1.5 + textSize),
                width - linePad, (float) (interval * 2.5 + textSize * 2), zPaint);

2.实现拖动效果

还记得之前的start吗,如果要移动整个布局,我是通过改变start来完成的,而start只是一个onDraw里的变量,所以需要一个全局变量来保存当前布局在y轴上移动的矢量距离

       float start = offSet;

这个offSet是在onTouchEvent里计算得出的,在手指按下时记录触摸事件的y坐标,在手指移动时,通过不断以上一次触摸事件(ACTION_DOWN或者ACTION_MOVE)的y坐标算差值,差值给offSet累加,并且刷新重新绘画。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                now = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                move = event.getY() - now;
                now = event.getY();
                offSet = offSet + move;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        return true;
    }

总结:拖动效果是通过计算每一次ACTION_DOWN、ACTION_MOVE事件的y坐标的差值,这个差值就是我们要移动的矢量距离

3.投掷效果的实现

 

关于投掷效果,主要通过计算脱手的一瞬间的速度,然后通过这个初速度计算移动距离和移动时间,将这个移动过程分成很多块来完成

 

首先介绍一些变量,投掷最小速度,投掷最大速度

        final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
        mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();

然后介绍一些函数,这些函数比较多,我就贴部分代码,但是作用有三个,通过初速度获取移动距离,通过距离获取初速度,通过初速度获取移动时间。这些计算公式是Android源码里的,我这里是直接借用,

    //获取滑动的时间
    private int getSplineFlingDuration(int velocit) {
        final double l = getSplineDeceleration(velocit);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return (int) (1000.0 * Math.exp(l / decelMinusOne));
    }
    //通过初始速度获取最终滑动距离
    private double getSplineFlingDistance(int velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }
    //通过需要滑动的距离获取初始速度
    public int getVelocityByDistance(double distance) {
        final double l = getSplineDecelerationByDistance(distance);
        int velocity = (int) (Math.exp(l) * mFlingFriction * mPhysicalCoeff / INFLEXION);
        return Math.abs(velocity);
    }

首先在获取ACTION_UP事件时,得到手指移动的初速度,这个由VelocityTracker来实现,在触发ACTION_DOWN事件时,初始化对象

    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

然后每一次的触摸事件都给了这个类对象

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }

最后在触发ACTION_UP事件时,通过VelocityTracker对象获取初速度,移动距离、移动时间,然后通过代码你们可以看得出来,我开启了一个周期任务,关于这个周期任务是实现fling投掷效果的关键。注意这个10

            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                if (Math.abs(velocityTracker.getYVelocity()) > mMinimumFlingVelocity) {
                    yVelocity = (int) velocityTracker.getYVelocity();
                    duration = getSplineFlingDuration(yVelocity);
                    distance = getSplineFlingDistance(yVelocity);
                    Log.v(TAG, "初速度:" + yVelocity + "移动距离:" + distance + "移动时间:" + duration);
                    if (flingmTask != null) {
                        flingmTask.cancel();
                        flingmTask = null;
                    }
                    flingmTask = new MyTimerTask(flingHandler);
                    flingtimer.schedule(flingmTask, 0, 10);
                }
                invalidate();
                break;
        }

直接来看看这个flingHandler写了啥,计算在10毫秒内以初速度做匀速运行的距离,然后把原来的移动距离减去10毫秒移动距离,再通过公式计算出以这个距离得出的初速度,覆盖掉原来的初速度,最后重新绘画。

直到这个初速度小于最小投掷速度,关闭周期任务。

    Handler flingHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (Math.abs(yVelocity) < mMinimumFlingVelocity) {
                yVelocity = 0;
                if (flingmTask != null) {
                    flingmTask.cancel();
                    flingmTask = null;
                }

            } else if (yVelocity != 0) {
                offSet = (float) (offSet + yVelocity * 0.01);
                double d = distance - Math.abs(yVelocity) * 0.01;
                yVelocity = getVelocityByDistance(distance) * yVelocity / Math.abs(yVelocity);
                distance = d;
                invalidate();
            }

        }
    };

总结:要实现投掷效果,就是将一段非匀速减速的运行变成一段段10毫秒匀速运动 

 

4.摆正效果的实现

首先我们要知道什么时候进行摆正,在我们完成投掷效果再进行摆正

    Handler flingHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (Math.abs(yVelocity) < mMinimumFlingVelocity) {
......
                guiwei();
            } else if (yVelocity != 0) {
.......
            }
        }
    };

 

因为手指离开手机的初速度可能为0,所以这个情况直接进行摆正

 

            case MotionEvent.ACTION_UP:

                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                if (Math.abs(velocityTracker.getYVelocity()) > mMinimumFlingVelocity) {
                    yVelocity = (int) velocityTracker.getYVelocity();
                    ......

                } else {
                    guiwei();
                }

 

额,那个guiwei函数就是我们摆正效果实现的核心代码,我们看看首先通过offSet的正负来区分出不同的方案,

 

首先我们来说一说offSet为负的情况,通过给offSet(布局偏差值)给 item的高度(textSize+interval)取余,赋值给surplus,然后通过判断这个surplus是否大于item的高度的一半,如果小于不作处理,就是说后面要给布局往下移动,如果大于那就得取 item的高度减去surplus的相反数,就是说要把布局往上移动。如果offSet为正,情况恰好相反

    public void guiwei() {
        if (offSet < 0) {
            surplus = (int) (-offSet) % (int) (textSize + interval);
            if (surplus < (textSize + interval) / 2) {
            } else {
                surplus = -(textSize + interval - surplus);
            }
        } else {
            surplus = (int) (offSet) % (int) (textSize + interval);
            if (surplus < (textSize + interval) / 2) {

                surplus = -surplus;
            } else {
                surplus = textSize + interval - surplus;
            }
        }
......
    }

最后我们同样是通过周期任务来实现摆正效果的实现

        if (mTask != null) {
            mTask.cancel();
            mTask = null;
        }
        mTask = new MyTimerTask(updateHandler);
        timer.schedule(mTask, 0, 10);

看看updateHandler,就是将这个摆正过程分成一段段匀速运动,规定每一次做匀速运行的距离。就是每隔10毫秒给offSet增加固定值,然后重新绘画

    Handler updateHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            if (Math.abs(surplus) < SPEED) {
                surplus = 0;
                if (mTask != null) {
                    mTask.cancel();
                    mTask = null;
                }
            } else if (surplus != 0) {
                offSet = surplus / Math.abs(surplus) * SPEED + offSet;
                surplus = (int) (surplus - surplus / Math.abs(surplus) * SPEED);
                invalidate();

            }


        }

    };

然后我们要添加选择回调就在之前的guiwei函数里完成,通过计算最后的offSet与item高度的取余来得出 选中item的位置,最后选的的item的位置,因为初始选中的是第二个,而初始位置可以往下拉一个位置,也就是头部选中时,offSet是一个item的高度的负值,所以所以我们这里需要加1

        currentItem= -(int) ((surplus+offSet)/(textSize+interval))+1;
        Log.v(TAG,"currentItem: "+currentItem);
        if(selectItemListener!=null){
            selectItemListener.selectItem(currentItem);
        }

总结:摆正效果就是,计算offSet偏差值与item的高度的取余,根据情况来判断是往下还是往上移动一段距离,来完成选中item的归位

 

 

 

5.完整代码

MyScrollView

public class MyScrollView extends View {

    public static final String TAG = "MyScrollView";

    Paint paint, zPaint;
    //数据源
    List<String> dataList;

    //字的大小
    int textSize = 50;
    //item之间的间距
    int interval = 30;

    int lineSize = 30;
    //偏差
    float offSet = 0;

    //view的宽高
    int width, height;

    //总高度
    int sumHeight;

    float centerY;
    float linePad = 30;

    float now;
    float move;

    private VelocityTracker mVelocityTracker;

    /**
     * 自动回滚到中间的速度
     */
    public static final float SPEED = 5;
    //甩手和归为
    private Timer timer;
    private MyTimerTask mTask;

    //归位,为了一定选中某个item
    private Timer flingtimer;
    private MyTimerTask flingmTask;
    Handler updateHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            if (Math.abs(surplus) < SPEED) {
                surplus = 0;
                if (mTask != null) {
                    mTask.cancel();
                    mTask = null;
                }
            } else if (surplus != 0) {
                offSet = surplus / Math.abs(surplus) * SPEED + offSet;
                surplus = (int) (surplus - surplus / Math.abs(surplus) * SPEED);
                invalidate();

            }


        }

    };

    //fling实现
    Handler flingHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (Math.abs(yVelocity) < mMinimumFlingVelocity) {
                yVelocity = 0;
                if (flingmTask != null) {
                    flingmTask.cancel();
                    flingmTask = null;
                }

                guiwei();


            } else if (yVelocity != 0) {
                offSet = (float) (offSet + yVelocity * 0.01);
                double d = distance - Math.abs(yVelocity) * 0.01;
                yVelocity = getVelocityByDistance(distance) * yVelocity / Math.abs(yVelocity);
                distance = d;
                invalidate();
            }

        }
    };

    public MyScrollView(Context context) {
        super(context);
        init();
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public void init() {


        paint = new Paint();
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.FILL);
        paint.setTextAlign(Paint.Align.CENTER);
        paint.setColor(Color.BLACK);
        paint.setTextSize(textSize);

        zPaint = new Paint();
        zPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        zPaint.setStyle(Paint.Style.FILL);
        zPaint.setTextAlign(Paint.Align.CENTER);
        zPaint.setColor(Color.BLUE);
        zPaint.setTextSize(textSize);

        dataList = new ArrayList<>();



        sumHeight = interval * (dataList.size() + 1) + textSize * dataList.size();

        centerY = (float) (interval + textSize * 1.5);

        timer = new Timer();
        flingtimer = new Timer();

        final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
        mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        minFLingDistance = getSplineFlingDistance(mMinimumFlingVelocity);
    }

    double minFLingDistance;


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //invalidate();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(dataList==null||dataList.size()==0){
            return ;
        }

        canvas.clipRect(0, 0, 300, (int) (3.5 * interval + textSize * 3));

        //滑动下限
        if (offSet <= -sumHeight + textSize * 2 + interval * 3) {
            offSet = -sumHeight + textSize * 2 + interval * 3;
        }

        //滑动上限
        if (offSet > textSize + interval) {
            offSet = textSize + interval;
        }

        float start = offSet;


        for (String s : dataList) {

            Rect rect = new Rect();
            paint.getTextBounds(s, 0, s.length(), rect);
            // 获取到的是实际文字宽度
            int textWidth = rect.width();

            start = start + interval;
            canvas.drawText(s, (width - textWidth) / 2, start + textSize, paint);
            start = start + textSize;

        }

        canvas.drawLine(linePad,
                (float) (interval * 1.5 + textSize), width - linePad, (float) (interval * 1.5 + textSize), zPaint);

        canvas.drawLine(linePad, (float) (interval * 2.5 + textSize * 2),
                width - linePad, (float) (interval * 2.5 + textSize * 2), zPaint);

        canvas.drawLine(linePad,
                (float) (interval * 1.5 + textSize), linePad, (float) (interval * 2.5 + textSize * 2), zPaint);

        canvas.drawLine(width - linePad, (float) (interval * 1.5 + textSize),
                width - linePad, (float) (interval * 2.5 + textSize * 2), zPaint);

    }

    int surplus;
    private int mMaximumFlingVelocity;
    private int mMinimumFlingVelocity;

    int yVelocity;

    int duration;
    double distance;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                now = event.getY();
                initOrResetVelocityTracker();
                break;
            case MotionEvent.ACTION_MOVE:
                move = event.getY() - now;
                now = event.getY();
                offSet = offSet + move;
                invalidate();

                break;
            case MotionEvent.ACTION_UP:

                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                if (Math.abs(velocityTracker.getYVelocity()) > mMinimumFlingVelocity) {
                    yVelocity = (int) velocityTracker.getYVelocity();

                    duration = getSplineFlingDuration(yVelocity);
                    distance = getSplineFlingDistance(yVelocity);

                    Log.v(TAG, "初速度:" + yVelocity + "移动距离:" + distance + "移动时间:" + duration);
                    if (flingmTask != null) {
                        flingmTask.cancel();
                        flingmTask = null;
                    }
                    flingmTask = new MyTimerTask(flingHandler);
                    flingtimer.schedule(flingmTask, 0, 10);

                } else {
                    guiwei();
                }
/*                if(offSet<0){
                    surplus= (int)(-offSet)%(int)(textSize+interval);
                    if(surplus<(textSize+interval)/2){

                    }else {
                        surplus=-(textSize+interval-surplus);
                    }
                }else {
                    surplus= (int)(offSet)%(int)(textSize+interval);
                    if(surplus<(textSize+interval)/2){

                        surplus=-surplus;
                    }else {
                        surplus=textSize+interval-surplus;
                    }
                }


                if (mTask != null)
                {
                    mTask.cancel();
                    mTask = null;
                }
                mTask = new MyTimerTask(updateHandler);
                timer.schedule(mTask, 0, 10);*/
                invalidate();

                break;
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }
        return true;
    }


    class MyTimerTask extends TimerTask {
        Handler handler;

        public MyTimerTask(Handler handler) {
            this.handler = handler;
        }

        @Override
        public void run() {
            handler.sendMessage(handler.obtainMessage());
        }

    }


    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

    private void resetVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        final float ppi = getContext().getResources().getDisplayMetrics().density * 160.0f;
        mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
                * 39.37f // inch/meter
                * ppi
                * 0.84f; // look and feel tuning
    }

    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)

    // A context-specific coefficient adjusted to physical values.
    private float mPhysicalCoeff;


    private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));

    // Fling friction
    private float mFlingFriction = ViewConfiguration.getScrollFriction();


    //获取滑动的时间
    private int getSplineFlingDuration(int velocit) {
        final double l = getSplineDeceleration(velocit);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return (int) (1000.0 * Math.exp(l / decelMinusOne));
    }

    private double getSplineDeceleration(float velocity) {
        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    }


    //通过初始速度获取最终滑动距离
    private double getSplineFlingDistance(int velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }

    //通过需要滑动的距离获取初始速度
    public int getVelocityByDistance(double distance) {
        final double l = getSplineDecelerationByDistance(distance);
        int velocity = (int) (Math.exp(l) * mFlingFriction * mPhysicalCoeff / INFLEXION);
        return Math.abs(velocity);
    }

    private double getSplineDecelerationByDistance(double distance) {
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return decelMinusOne * (Math.log(distance / (mFlingFriction * mPhysicalCoeff))) / DECELERATION_RATE;
    }

    int currentItem;

    public void guiwei() {
        if (offSet < 0) {
            surplus = (int) (-offSet) % (int) (textSize + interval);
            if (surplus < (textSize + interval) / 2) {

            } else {
                surplus = -(textSize + interval - surplus);
            }
        } else {
            surplus = (int) (offSet) % (int) (textSize + interval);
            if (surplus < (textSize + interval) / 2) {

                surplus = -surplus;
            } else {
                surplus = textSize + interval - surplus;
            }
        }

        currentItem= -(int) ((surplus+offSet)/(textSize+interval))+1;
        Log.v(TAG,"currentItem: "+currentItem);
        if(selectItemListener!=null){
            selectItemListener.selectItem(currentItem);
        }


        if (mTask != null) {
            mTask.cancel();
            mTask = null;
        }
        mTask = new MyTimerTask(updateHandler);
        timer.schedule(mTask, 0, 10);
    }

    public interface SelectItemListener{
        public void selectItem(int index);
    }

    private SelectItemListener selectItemListener;

    public void setSelectItemListener(SelectItemListener selectItemListener){
        this.selectItemListener=selectItemListener;
    }

    public void setDataList(List<String> dataList){
        this.dataList=dataList;
        sumHeight = interval * (dataList.size() + 1) + textSize * dataList.size();

        invalidate();
    }
}

MainActivity

public class MainActivity extends Activity {

	MyScrollView mySV;
	TextView tv;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mySV=findViewById(R.id.mySV);
		tv=(TextView)findViewById(R.id.tv);
		mySV.setSelectItemListener(new MyScrollView.SelectItemListener() {
			@Override
			public void selectItem(int index) {
				tv.setText(""+index);
			}
		});
		List<String> dataList=new ArrayList<>();
		for (int i = 0; i < 30; i++) {
			dataList.add("" + i);
		}
		mySV.setDataList(dataList);

	}


}

 

activity_main

 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <com.example.autoclickdemo.MyScrollView
        android:id="@+id/mySV"
        android:layout_centerInParent="true"
        android:layout_width="100dp"
        android:layout_height="100dp" />

</RelativeLayout>



 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值