2018/10/27 修改
没有首尾连接,可以向上向下拉出,然后弹回
放手后有短暂的"回弹"的动画
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即可。