Android 一个简单的自定义WheelView实现

2018/10/27 修改

效果图:

Android 一个简单的自定义WheelView实现_第1张图片

 

没有首尾连接,可以向上向下拉出,然后弹回

放手后有短暂的"回弹"的动画

 

代码:

package com.example.crazyflower.mywheelview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.lang.ref.WeakReference;
import java.util.List;


/**
 * Created by CrazyFlower on 2018/4/2.
 */

public class MyWheelView extends View {

    private static final String TAG = "MyWheelView";

    private static final String RESILIENCE_DISTANCE_OF_ONCE = "resilience_distance_of_once";
    private static final String RESILIENCE_LEFT_TIMES = "left_times";

    private static final int RESILIENCE_TIMES = 5;
    private static final int RESILIENCE_TIME_INTERVAL = 50;

    private List data;
    private int selectedItemIndex = 0;

    private float lastY;
    private float scrollY;

    private int viewWidth;
    private int viewHeight;
    private float itemHeight;
    private int itemNumber;
    private int halfItemNumber;
    private static final float maxScaleTextSizeToItemHeight = 0.9f;
    private static final float minScaleTextSizeToItemHeight = 0.72f;
    private float maxTextSize;
    private float minTextSize;

    private IWheelViewSelectedListener wheelViewSelectedListener;

    Paint selectedLinePaint;
    Paint selectedBackgroundPaint;
    Paint normalTextPaint;
    Paint selectedTextPaint;

    private Handler handler;

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

    public MyWheelView(Context context, AttributeSet attrs) {
        this(context, attrs, 1);
    }

    public MyWheelView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyWheelView);
        initAttributesData(typedArray);
        initDefaultData();
    }

    private void initAttributesData(TypedArray typedArray) {
        Log.d(TAG, "initDataAndPaint: ");

        itemNumber = typedArray.getInt(R.styleable.MyWheelView_item_number, 5);
        halfItemNumber = itemNumber / 2;

        selectedLinePaint = new Paint();
        selectedBackgroundPaint = new Paint();
        normalTextPaint = new Paint();
        selectedTextPaint = new Paint();

        selectedLinePaint.setColor(typedArray.getColor(R.styleable.MyWheelView_selected_line_color, Color.rgb(0, 0, 0)));
        selectedBackgroundPaint.setColor(typedArray.getColor(R.styleable.MyWheelView_selected_background_color, Color.rgb(255, 255, 255)));
        normalTextPaint.setColor(typedArray.getColor(R.styleable.MyWheelView_normal_text_color, Color.rgb(0, 0, 0)));
        selectedTextPaint.setColor(typedArray.getColor(R.styleable.MyWheelView_selected_text_color, Color.rgb(0, 255, 204)));

        selectedLinePaint.setStyle(Paint.Style.FILL_AND_STROKE);
        selectedLinePaint.setStrokeWidth(4);
        selectedBackgroundPaint.setStyle(Paint.Style.FILL);
    }

    /*
     * 初始化宽高有关的数据
     */
    private void initWHData() {
        viewWidth = getMeasuredWidth();
        viewHeight = getMeasuredHeight();
        itemHeight = ((float) viewHeight) / itemNumber;
        maxTextSize = maxScaleTextSizeToItemHeight * itemHeight;
        minTextSize = minScaleTextSizeToItemHeight * itemHeight;
    }

    private void initDefaultData() {
        //默认选中为0,实际上setData的时候也会初始化selectedItemIndex
        selectedItemIndex = 0;

        handler = new MyWheelViewHandler(this);
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(TAG, "onDraw: " + selectedItemIndex);

        //如果没有数据或者数据量为0,不绘制
        if (null == data || 0 == data.size()) {
            return;
        }

        drawSelectedRectangle(canvas);

        /*
         * draw the text
         * think about the effect, draw the selected, and (halfItemNumber + 1) items above it,
         * and (halfItemNumber + 1) items below it.
         */
        String text;
        Paint paint;
        float midY;
        for (int i = Math.max(0, selectedItemIndex - (halfItemNumber + 1)),
             max = Math.min(data.size() - 1, selectedItemIndex + (halfItemNumber + 1));
             i <= max; i++) {
            text = data.get(i);

            midY = itemHeight * (halfItemNumber - (selectedItemIndex - i)) + itemHeight / 2 - scrollY;
            if (i == selectedItemIndex)
                paint = selectedTextPaint;
            else
                paint = normalTextPaint;

            setTextPaint(paint, midY);
            canvas.drawText(text, (viewWidth - getTextWidth(paint, text)) / 2,
                    midY + getTextBaselineToCenter(paint), paint);
        }
    }

    //绘制选中item的背景和线条
    private void drawSelectedRectangle(Canvas canvas) {
        canvas.drawLine(0, itemHeight * halfItemNumber, viewWidth, itemHeight * halfItemNumber, selectedLinePaint);
        canvas.drawLine(0, itemHeight * (halfItemNumber + 1), viewWidth, itemHeight * (halfItemNumber + 1), selectedLinePaint);
        canvas.drawRect(0, itemHeight * halfItemNumber, viewWidth, itemHeight * (halfItemNumber + 1), selectedBackgroundPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: " + event.getAction() + " " + event.getY());
        Message message;
        Bundle bundle;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handler.removeMessages(MyWheelViewHandler.RESILIENCE);
                lastY = event.getY();
                return true;
            case MotionEvent.ACTION_MOVE:
                scrollY -= event.getY() - lastY;
                lastY = event.getY();
                confirmSelectedItem();
                return true;
            case MotionEvent.ACTION_UP:
                message = handler.obtainMessage();
                message.what = MyWheelViewHandler.RESILIENCE;
                bundle = new Bundle();
                bundle.putFloat(RESILIENCE_DISTANCE_OF_ONCE, scrollY / RESILIENCE_TIMES);
                bundle.putInt(RESILIENCE_LEFT_TIMES, RESILIENCE_TIMES);
                message.setData(bundle);
                message.sendToTarget();
                return true;
        }
        return false;
    }

    /**
     * @return 该字串在width下 字串中间对齐控件中间时候的 drawText用的x
     */
    private float getTextWidth(Paint paint, String text) {
        return paint.measureText(text);
    }

    /**
     * @return 该字串在itemHeight下 字串中间对齐控件中间的baseLine的y
     */
    private float getTextBaselineToCenter(Paint paint) {
        Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
        return ((float) (- fontMetrics.bottom - fontMetrics.top)) / 2;
    }

    private void confirmSelectedItem() {
        //计算移动了几个item的height了, < 0说明向上, >0说明向下
        int changedItemNumber = Math.round(scrollY / itemHeight);

        int lastItem = getSelectedItemIndex();
        //计算这次的【合法的】的index
        int tempSelectedItem = getSelectedItemIndex() + changedItemNumber;
        if (tempSelectedItem < 0)
            tempSelectedItem = 0;
        if (tempSelectedItem >= data.size())
            tempSelectedItem = data.size() - 1;
        this.selectedItemIndex = tempSelectedItem;
        //减去相应的scrollY值(为了可以上滑和下滑超出)
        scrollY -= itemHeight * (selectedItemIndex - lastItem);
        invalidate();
        if (lastItem != tempSelectedItem)
            noticeListener();
    }

    private void setTextPaint(Paint paint, float midY) {
        paint.setTextSize(maxTextSize - (maxTextSize - minTextSize) * Math.abs(viewHeight / 2 - midY) / (viewHeight / 2) );
    }

    public int getSelectedItemIndex() {
        return selectedItemIndex;
    }


    /*
     * 这个是专门给外部类调用设置用的,类内不应该调用。 也是因为类内不用主动
     */
    public void setDataWithSelectedItemIndex(List data, int selectedItemIndex) {
        this.data = data;
        setSelectedItemIndex(selectedItemIndex);
    }

    /*
     * 这个是专门给外部类调用设置用的,类内不应该调用。 也是因为类内不用主动
     */
    public void setSelectedItemIndex(int selectedItemIndex) {
        //外部自己负责处理这个index是否合法
        this.selectedItemIndex = selectedItemIndex;
        //既然外部设置index,就不要这个偏移量了
        this.scrollY = 0;
        invalidate();
        noticeListener();
    }

    private void resilienceToCenter(float distance) {
        scrollY -= distance;
        invalidate();
    }

    private void noticeListener() {
        if (null != wheelViewSelectedListener)
            wheelViewSelectedListener.wheelViewSelectedChanged(this, data, selectedItemIndex);
    }

    public void setWheelViewSelectedListener(IWheelViewSelectedListener wheelViewSelectedListener) {
        this.wheelViewSelectedListener = wheelViewSelectedListener;
    }

    private static class MyWheelViewHandler extends Handler {

        static final int RESILIENCE = 1;

        private WeakReference myWheelViewWeakReference;

        private MyWheelViewHandler(MyWheelView myWheelView) {
            myWheelViewWeakReference = new WeakReference<>(myWheelView);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            MyWheelView myWheelView;
            Message message;
            Bundle bundle;
            int leftTimes;
            switch (msg.what) {
                case RESILIENCE:
                    bundle = msg.getData();
                    myWheelView = myWheelViewWeakReference.get();
                    if (null != myWheelView)
                        myWheelView.resilienceToCenter(bundle.getFloat(RESILIENCE_DISTANCE_OF_ONCE, 0));

                    leftTimes = bundle.getInt(RESILIENCE_LEFT_TIMES, 0);
                    if (leftTimes > 1) {//如果还要重绘,发送重绘的消息,这里重复使用了bundle
                        bundle.putInt(RESILIENCE_LEFT_TIMES, leftTimes - 1);
                        message = new Message();
                        message.what = RESILIENCE;
                        message.setData(bundle);
                        this.sendMessageDelayed(message, RESILIENCE_TIME_INTERVAL);
                    }
                    break;
            }
        }
    }

}

 

详细:

    重要变量:

        scrollY:当前画面  对于  把selectedItemIndex放在正中间时的画面  的垂直偏移量。如效果图中所示,如果当前选中项的下标为0,且上拉超出了很多,该值应该很大。当仅仅是小小拉动离开中间位置一点点的时候,该值应该很小。该值由onTouchEvent中的event处理得到。

        handler:用于处理“回弹”效果时的消息传递者(由于要在主线程中更新)。

    重要方法:

        onDraw(),进行绘制。 没有数据的话不进行绘制。调用drawSelectedRectangle()绘制中间那个凸显选中的矩形。然后绘制被选中项上下(halfItemNumebr + 1)项(这几项可以被看到)。如果没有scrollY的话,每一项都在固定位置上,selected那项就在正中间,他的前一项后一项就在上下那个位置。但这显然不是我们要的效果。为了在滑动中可以显示滑倒一半的那种效果,加了scrollY,目的如上所说。midY是该项item垂直中线的Y的值,setText是根据该项item的midY离view中间的距离设置画笔的textSize,getTextWidth是用来获得字串画出来的宽度,getBaseLineToCenter返回的是text的baseline到item中间的距离。

        onTouchEvent(),接受触摸事件,我本来想用GestureDetector帮我代劳,我写几个接口的处理就好(特别是onFling,滑动中很有用),但是GestureDetector中没有单独的ACTION_UP动作的接口,如果使用GestureDetector,然后自己再写ACTION_UP的处理,感觉有点头疼,就自己处理了。在这个过程中,ACTION_DOWN,起始触摸动作,我在这里把handler中的消息都清光了,为的是:如果上一次的回弹动画还没结束,则我应该把回弹动画取消掉了,手指按下的时候就直接在当前画面下继续。ACTION_MOVE:手指滑动,这里处理事件的时候,注意手指向下拉的时候,列表上滑,手指向上拉的时候,列表上滑。注意加减和正负号关系。对于每一次MOVE动作,利用lastY和event.getY()获取偏移量,加到scrollY中,即可获得整次手指操作的偏移量,然后在confirmSelectedItem中判断当前选中哪个了,在confirmSelectedItem中会调用重绘函数。对于ACTION_UP:手指停止滑动,在这里处理回弹(即偏移中间的回到中间,实际上就是每隔一段重新设定scrollY,然后重绘,给使用者一种动画的感觉,我在这里是每隔50ms,一次回弹重绘5次),用handler和message处理实现。自己实现onFling除了记录时间,感觉没有很好的思路,以后可能会加上。

        confirmSelectedItem(),根据scrollY,判断当前应该是选个最靠近中间,然后修改selectedItemIndex,同时修改scrollY,这两个要一起修改,这样才能保持一致,然后重绘,然后与之前的selectedItemIndex比较,如果不同,调用监听者的函数

 

使用:




    


    

item_number,selected_background_color,selected_line_color,selected_text_color,自定义View属性。加在attrs.xml中



    
        
        
        
        
        
    

效果看名字应该看得出来了2333。

然后是在代码中的使用:

package com.example.crazyflower.mywheelview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;

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

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private TextView textView;

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

        MyWheelView myWheelView = findViewById(R.id.test_wheelview);

        List data = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            data.add(i + "");
        }
        data.add("123453212");
        data.add("123jhfsagdfsdafds");
        for (int i = 0; i < 10; i++) {
            data.add(i + "");
        }
        myWheelView.setDataWithSelectedItemIndex(data, 0);

        textView = findViewById(R.id.selected_text);

        myWheelView.setWheelViewSelectedListener(new IWheelViewSelectedListener() {
            @Override
            public void wheelViewSelectedChanged(MyWheelView myWheelView, List data, int position) {
                Log.d(TAG, "wheelViewSelectedChanged: " + data.get(position));
                textView.setText(data.get(position));
            }
        });
    }
}

使用的时候只要设置data和初始的selected_item,然后需要监听的话可以设置监听的,不需要监听的话,只需要在合适的时候调用getSelectedItemIndex()就好了。

注意:先设置监听,再设置data的话,会通知监听者。先设置data,再设置监听的话,则不会。这个主要是在初始化的时候,看你的需求。

联动的话:在某个wheelview监听者的函数被调用后,调用另外一个wheelview的setDateWithIndex即可。

 

参考:https://blog.csdn.net/junzia/article/details/50979382

整个项目:https://github.com/AngrySwordCrazyFlower/MyWheelView

如有错误,欢迎指出

 

你可能感兴趣的:(Android)