android滾輪選擇器詳細教程
最近在學習自定義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 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" tools:context="com.androidtest.MainActivity" > <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:gravity="center_vertical" android:text="單位" android:layout_width="wrap_content" android:layout_alignParentRight="true" android:layout_height="fill_parent"/> <ScrollView android:layout_height="300dp" android:layout_width="fill_parent" android:layout_centerHorizontal="true" android:scrollbars="none" android:overScrollMode="never" android:layout_toLeftOf="@id/aa" > <LinearLayout android:orientation="vertical" android:layout_gravity="center_horizontal" android:layout_height="300dp" android:layout_width="wrap_content" > <!-- <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:background="@drawable/btton_avtie" android:text="確定" android:layout_weight="3" android:textSize="30dp" /> </LinearLayout> </RelativeLayout> </RelativeLayout>
附上效果圖:
相信大家看佈局檔案還是看得懂的,第二個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
<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"
tools:context="com.androidtest.MainActivity" >
<com.androidtest.WheelView
android:id="@+id/wheelview"
android:layout_width="wrap_content"
android:background="@drawable/act_menu_bg_hover"
android:layout_height="wrap_content"/>
</RelativeLayout>
注意,我們剛才的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,幾個因為解決了,相信大家看著我的程式碼,應該豁然開朗了