最近在学习自定义android控件,碰上了滚动选择器的效果,自己找来了别人代码,一眼看上去晕头转向(因为缺少经验),第二天静下心来,才能透过现象看本质,觉得写一篇博文帮助其他同学。
下面先看效果图
何为本质,就是思路,我一开始看到这个就懵了,没有思路,后来看了别人的代码,才明白是怎么一回事,凭着自己的理解,居然非常顺利的就写出来了。
思路:1,布局,整个控件的布局,其实就是用代码取带xml来实现当前布局
2,可以滑动的(即滚轮),其实是一个ScrollView
3,判断滑动状态的,有protected void onScrollChanged(int x, int y, int oldx, int oldy) 方法,可以为我们获得当前y值(一开始y=0;随着滑动,y开始增大)
那么我们首先来完成第一个,为了思考方便,我先用xml搭建出了控件的样子,然后我们再用代码去实现,事实证明,这样的思路行云流水
下面,我们来看这个test.xml
附上效果图:
相信大家看布局文件还是看得懂的,第二个relativeLayout就是控件,我们的任务就是把这些xml写成代码(有些个别设置与xml的不同,注意属性的差别)
我决定分三个类,第一个是WheelView,来表示这个控件,也就是说它便是第二个relativeLayout
第二个类是CheckNumView,它表示第三个relativeLayout
第三个类是WheelScrollView,它表示ScrollView
显然,这个三个类的关系很清楚,就是后一个嵌套在前一个里面
至于其他控件,例如确定按钮,大家看布局文件就应该可以加上
下面我从MainActivity开始说起,为了表示轮子,我建立了一个JAVABEAN,也就是Wheel类,这个类存储每个轮子里面的数据。
package com.androidtest;
public class Wheel {
/**
* 内容
*/
private String[] texts;
/**
* 焦点文字
*/
private String focusText;
public Wheel(String[] texts){
this.texts = texts;
}
public String[] getTexts() {
return texts;
}
public int getFocusTextPosition() {
int position = 0;
int count = texts.length;
if(count > 0 ){
for (int i = 0; i < texts.length; i++) {
if(texts[i].equals(focusText)) {
position = i;
}
}
if(position == 0) {
position = -1;
}
}else{
position = -1;
}
return position;
}
}
然后是Activity
package com.androidtest;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
public class MainActivity extends ActionBarActivity {
WheelView wheelView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wheelView = (WheelView) findViewById(R.id.wheelview);
String[] years = {"1999","2000","2001","2002","2003","2004","2005","2006","2007","2008","2009","2010","2011","2012"};
String[] mons = {"1999","2000","2001","2002","2003","2004","2005","2006","2007","2008","2009","2010","2011","2012"};
Wheel w1 = new Wheel(years);
Wheel w2 = new Wheel(mons);
Wheel[] ws = {w1,w2};
wheelView.setWheels(ws);
}
}
从上面的代码看出,我的初衷就是,每创建一个轮子Wheel,将它加入数组,就可以动态增加轮子
再看activity_main.xml
注意,我们刚才的test.xml只是为了我思考方便的,实际上并不需要用到,真正在布局的,是在爱activity_main.xml里面增加自定义控件
然后是WheelView
package com.androidtest;
import java.util.Arrays;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.RelativeLayout.LayoutParams;
public class WheelView extends RelativeLayout {
static int rowHeight = 100;
Context context;
private CheckNumView[] numberViews;
public WheelView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
/**
* 获得结果
* @return
*/
public String[] getResult(){
String[] nums = new String[numberViews.length];
for (int i = 0; i < numberViews.length; i++) {
nums[i] = numberViews[i].getNumber();
}
return nums;
}
@SuppressLint("NewApi")
public void setWheels(Wheel[] wheels) {
//轮子数组
numberViews = new CheckNumView[wheels.length];
//中间蓝色的遮蔽层
ImageView imageView = new ImageView(context);
imageView.setBackgroundResource(R.drawable.shoukuan_border4);
RelativeLayout.LayoutParams lp1 = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
lp1.height = rowHeight;
lp1.setMargins(0, rowHeight, 0, 0);
imageView.setLayoutParams(lp1);
addView(imageView);
//下面就是包裹滚轮的LinearLayout
LinearLayout llayout = new LinearLayout(context);
LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
lp2.height = rowHeight*3;
llayout.setOrientation(LinearLayout.HORIZONTAL);
llayout.setLayoutParams(lp2);
//将滚轮添加到LinearLayout里面
int i = 0;
for(Wheel wheel : wheels){
RelativeLayout rlayout = new RelativeLayout(context);
LinearLayout.LayoutParams lp3 = new LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
lp3.width = 0;
lp3.weight = 4;
numberViews[i] = new CheckNumView(context,wheel);
llayout.addView(numberViews[i],lp3);
i++;
}
//右边的确定按钮
Button btn = new Button(context);
btn.setText("确定");
btn.setTextSize(30);
btn.setBackgroundResource(R.drawable.btton_avtie);
LinearLayout.LayoutParams lp4 = new LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
lp4.gravity = Gravity.CENTER;
lp4.weight = 3;
//点击按钮,弹出选中数据
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, Arrays.toString(getResult()), Toast.LENGTH_SHORT).show();;
}
});
llayout.addView(btn,lp4);
addView(llayout);
}
}
然后是CheckNumView,其实每个CheckNumView就是单独一个滚轮,然而它仍然是继承RelativeLayout,而不是ScroolView,是为了更方便的调整滚轮的位置,况且,滚轮旁边还有一个标志单位的TextView,显然它个滚轮(ScroolView)应该是一个整体,所以我们把ScroolView和单位TextView先包装成一个整体
package com.androidtest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.view.Gravity;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.androidtest.WheelScrollView.OnScrollStopListener;
public class CheckNumView extends RelativeLayout{
WheelScrollView sc;
String[] texts;
private int currentY = -1000;
private int position = 1;
public CheckNumView(Context context) {
super(context);
}
@SuppressLint("NewApi")
public CheckNumView(Context context, Wheel wheel) {
super(context);
//获取数据字符串数组
texts = wheel.getTexts();
//这个RelativeLayout用于包裹滚轮
RelativeLayout rlayout = new RelativeLayout(context);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
rlayout.setGravity(Gravity.CENTER);
//单位TextView
TextView unit = new TextView(context);
unit.setText("单位");
unit.setId(1111);
unit.setGravity(Gravity.CENTER);
RelativeLayout.LayoutParams lp2 = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
lp2.addRule(RelativeLayout.CENTER_VERTICAL,RelativeLayout.TRUE );
lp2.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
rlayout.addView(unit,lp2);
//滚轮
sc = new WheelScrollView(context,texts);
sc.setVerticalScrollBarEnabled(false);
sc.setHorizontalScrollBarEnabled(false);
sc.setOverScrollMode(OVER_SCROLL_NEVER);
RelativeLayout.LayoutParams lp3= new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
lp3.height = WheelView.rowHeight * 3;
lp3.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE );
lp3.addRule(RelativeLayout.LEFT_OF, 1111);
//这个方法,是指滚轮初始化以后的第一个位置
sc.post(new Runnable() {
@Override
public void run() {
sc.scrollTo(0, 0*WheelView.rowHeight);
}
});
//这个方法,设置选中位置字符串的颜色
setFocusText(1);
/*
* 这个是回调监听器,一旦滚轮停止滚动,就是触发
* 有必要说下的是,currentY必须不断更新
*/
sc.setOnScrollStopListener(new OnScrollStopListener(){
@Override
public void onStop(int y) {
if(y != currentY) {
// 判断滚动误差,不到行高的一半就抹掉,超过行高的一半而不到一个行高就填满
if (y % WheelView.rowHeight >= (WheelView.rowHeight / 2)) {
y = y + WheelView.rowHeight - y % WheelView.rowHeight;
sc.scrollTo(0, y);
} else {
y = y - y % WheelView.rowHeight;
sc.scrollTo(0, y);
}
setFocusText(y / WheelView.rowHeight+1);
}
currentY = y;
}
});
rlayout.addView(sc,lp3);
addView(rlayout,lp);
}
/**
* 设置焦点文字风格
*
* @param position
*/
private void setFocusText(int position) {
if(this.position >= 0) {
sc.textViews[this.position].setTextColor(Color.BLACK);
}
sc.textViews[position].setTextColor(Color.RED);
this.position = position;
}
public String getNumber() {
return texts[position-1];
}
}
看到这里,如果有人被弄糊涂了,那么请记住我上面给出的第一个任务,实现布局。
至于这里的setOnScrollStopListener方法,我们可以暂时不管它,因为它与布局的师兄无关
最后再看WheelScrollView
package com.androidtest;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;
public class WheelScrollView extends ScrollView implements Runnable{
private String[] texts;
public boolean isStop = false;
private Thread t;
private int y;
private int curY = 0;
public TextView[] textViews;
/*
* 使用handler是为了修改主线程ui,也就是CheckNumView里面的setFocusText()方法
* 如果不需要改变ui,我大可不必使用handler,直接用一个子线程来通知listener就可以了
*/
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (isStop) {
listener.onStop(curY);
isStop = false;
}
y = -100;
curY = 0;
}
};
//监听器
private OnScrollStopListener listener;
public WheelScrollView(Context context,String[] texts) {
super(context);
this.texts = texts;
//scrollview里面的textViews
textViews = new TextView[texts.length+2];
//scrollview里面LinearLayout
LinearLayout llayout = new LinearLayout(context);
RelativeLayout.LayoutParams lp4= new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
lp4.height = WheelView.rowHeight * 3;
lp4.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE );
llayout.setOrientation(LinearLayout.VERTICAL);
/*
* 下面将textViews逐一加到LinearLayout里面
* 并且设置头一个空白的textViews,跟尾一个空白的textViews,这样的目的是因为我们选中的项是在中间
*/
RelativeLayout.LayoutParams lp5= new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
lp5.height = WheelView.rowHeight;
textViews[0] = new TextView(context);
textViews[0].setGravity(Gravity.CENTER_VERTICAL);
textViews[0].setText("");
llayout.addView(textViews[0],lp5);
int i = 1;
for(String text :texts){
textViews[i] = new TextView(context);
textViews[i].setGravity(Gravity.CENTER_VERTICAL);
textViews[i].setText(text);
llayout.addView(textViews[i],lp5);
i++;
}
textViews[i] = new TextView(context);
textViews[i].setGravity(Gravity.CENTER_VERTICAL);
textViews[i].setText("");
llayout.addView(textViews[i],lp5);
//将LinearLayout加入ScrollView
addView(llayout,lp4);
}
//滚动时自动调用该函数,获取y值
@Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
super.onScrollChanged(x, y, oldx, oldy);
this.y = y < 0 ? 0 : y;
}
//回调接口
public static interface OnScrollStopListener {
public void onStop(int y);
}
public void setOnScrollStopListener(OnScrollStopListener listener) {
this.listener = listener;
}
//减少滚动的速度
@Override
public void fling(int velocityY) {
super.fling(velocityY / 3);
}
//这里是判断滚动触发开始,与滚动触发停止的
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_UP){
isStop = true;
if (t == null) {
t = new Thread(this);
t.start();
} else if (!t.isAlive()) {
t = new Thread(this);
t.start();
}
}else if(ev.getAction()==MotionEvent.ACTION_DOWN){
isStop = false;
}
return super.onTouchEvent(ev);
}
//如果通知滚动,新线程将使用handler请求修改ui,并且调用回调函数,式选项在正确的位置上
@Override
public void run() {
while (isStop) {
try {
if (curY == y) {
handler.sendEmptyMessage(0);
} else {
curY = y;
}
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
同样,如果这都代码看不懂,你可以先忽略一些与布局无关的东西(除了构造函数,基本其他函数都与布局无关)。
忽略这些代码以后,我相信已经可以画出这个控件,并且可以拖动了
下面的问题就是我们希望拖到两个选项中间,脱手时,会自动对准某一个最近的选项
这是我们就需要用到其他的代码了。
思路是使用onTouchEvent(MotionEvent ev)来判断滑动开始与结束
一点滑动结束,我们就要拿到当前的y值,然后通过一个线程,调用handler去通知CheckNumView里面的OnScrollStopListener,最后我们在onstop()函数里面,处理这个y值
一个疑问是为什么获得y值以后,要通过线程调用handler,理由是防止再次TouchEvent影响前一次TouchEvent的结果
第二个疑问是,为什么要记录curY,因为只有curY==y,我们才能确定滑动停止了
OK,几个因为解决了,相信大家看着我的代码,应该豁然开朗了
源码下载