思路:
1,布局,整个控件的布局,其实就是用代码取带xml来实现当前布局
2,可以滑动的(即滚轮),其实是一个ScrollView
3,判断滑动状态的,有protected void onScrollChanged(int x, int y, int oldx, int oldy) 方法,可以为我们获得当前y值(一开始y=0;随着滑动,y开始增大)
那么我们首先来完成第一个,为了思考方便,我先用xml搭建出了控件的样子,然后我们再用代码去实现,事实证明,这样的思路行云流水
下面,我们来看这个test.xml
<?xml version="1.0" encoding="utf-8"?> <!-- 这个布局文件只是为了思考方便的,实际上并不需要用到,真正在布局的,是在main.xml里面增加自定义控件 --> <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" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" > <RelativeLayout android:id="@+id/relativeLayout1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/act_menu_bg_hover" > <ImageView android:layout_width="match_parent" android:layout_height="100dp" android:layout_marginTop="100dp" android:background="@drawable/shoukuan_border4" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="300dp" android:orientation="horizontal" > <RelativeLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="4" > <RelativeLayout android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center" > <TextView android:id="@+id/aa" android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_alignParentRight="true" android:gravity="center_vertical" android:text="单位" /> <ScrollView android:layout_width="fill_parent" android:layout_height="300dp" android:layout_centerHorizontal="true" android:layout_toLeftOf="@id/aa" android:overScrollMode="never" android:scrollbars="none" > <LinearLayout android:layout_width="wrap_content" android:layout_height="300dp" android:layout_gravity="center_horizontal" android:orientation="vertical" > <!-- <TextView android:layout_height="100dp" android:layout_width="wrap_content" android:textAlignment="center" android:text=""/> <TextView android:layout_height="100dp" android:layout_width="wrap_content" android:textAlignment="center" android:gravity="center_vertical" android:text="1999"/> <TextView android:layout_width="wrap_content" android:layout_height="100dp" android:text="2000" android:gravity="center_vertical" android:textAlignment="center" /> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:text="2001"/> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:text="2002"/> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:text="2003"/> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:text="2004"/> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:text="2005"/> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:text="2006"/> <TextView android:layout_height="100dp" android:gravity="center_vertical" android:layout_width="wrap_content" android:textAlignment="center" android:text=""/> --> </LinearLayout> </ScrollView> </RelativeLayout> </RelativeLayout> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="3" android:background="@drawable/btton_avtie" android:text="确定" android:textSize="30dp" /> </LinearLayout> </RelativeLayout> </RelativeLayout>
相信大家看布局文件还是看得懂的,第二个relativeLayout就是控件,我们的任务就是把这些xml写成代码(有些个别设置与xml的不同,注意属性的差别)
我决定分三个类,第一个是WheelView,来表示这个控件,也就是说它便是第二个relativeLayout
第二个类是CheckNumView,它表示第三个relativeLayout
第三个类是WheelScrollView,它表示ScrollView
显然,这个三个类的关系很清楚,就是后一个嵌套在前一个里面
至于其他控件,例如确定按钮,大家看布局文件就应该可以加上
下面我从MainActivity开始说起,为了表示轮子,我建立了一个JAVABEAN,也就是Wheel类,这个类存储每个轮子里面的数据。
package com.xp.demo; /** * 实体类 这个类存储每个轮子里面的数据 * * @author sooner * */ 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; } }然后是MainActivity
package com.xp.demo; import android.app.Activity; import android.os.Bundle; public class MainActivity extends Activity { /** * 思路: 1,布局,整个控件的布局,其实就是用代码取带xml来实现当前布局 2,可以滑动的(即滚轮),其实是一个ScrollView * 3,判断滑动状态的,使用: protected void onScrollChanged(int x, int y, int oldx, int * oldy) 方法 */ WheelView wheelView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); wheelView = (WheelView) findViewById(R.id.wheelview); String[] years = { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23" }; String[] mons = { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59" }; // 每创建一个轮子Wheel,将它加入数组,就可以动态增加轮子 Wheel w1 = new Wheel(years); Wheel w2 = new Wheel(mons); Wheel[] ws = { w1, w2 }; wheelView.setWheels(ws); } }
从上面的代码看出,我的初衷就是,每创建一个轮子Wheel,将它加入数组,就可以动态增加轮子
再看main.xml
<?xml version="1.0" encoding="utf-8"?> <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" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" > <com.xp.demo.WheelView android:id="@+id/wheelview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/act_menu_bg_hover" /> </RelativeLayout>
注意,我们刚才的test.xml只是为了我思考方便的,实际上并不需要用到,真正在布局的,是在爱activity_main.xml里面增加自定义控件
然后是WheelView
package com.xp.demo; import java.util.Arrays; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.Toast; 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.xp.demo; 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.xp.demo.WheelScrollView.OnScrollStopListener; /** * 每个CheckNumView就是单独一个滚轮, * 然而它仍然是继承RelativeLayout,而不是ScroolView, * 是为了更方便的调整滚轮的位置,况且,滚轮旁边还有一个标志单位的TextView, * 显然它个滚轮(ScroolView)应该是一个整体, * 所以我们把ScroolView和单位TextView先包装成一个整体 * @author sooner * */ 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方法,我们可以暂时不管它,因为它与布局的实现无关
package com.xp.demo; 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,几个因为解决了,相信大家看着我的代码,应该豁然开朗了。
参考博客:http://blog.csdn.net/crazy__chen/article/details/41958677
参考源码:http://download.csdn.net/detail/u010963246/8922921
源码下载:http://download.csdn.net/detail/u010963246/8922911
版权声明:本文为博主原创文章,未经博主允许不得转载。