Android自定义控件实战——滚动选择器PickerView


  手机里设置闹钟需要选择时间,那个选择时间的控件就是滚动选择器,前几天用手机刷了MIUI,发现自带的那个时间选择器效果挺好看的,于是就自己仿写了一个,权当练手。先来看效果:

                                                                 Android自定义控件实战——滚动选择器PickerView_第1张图片

效果还行吧?实现思路就是自定义一个PickerView,单独滚动的是一个PickerView,显然上图中有分和秒的选择所以在布局里用了两个PickerView。由于这里不涉及到text的点击事件,所以只需要继承View就行了,直接把text用canvas画上去。PickerView的实现的主要难点:

难点1:

        字体随距离的渐变。可以看到,text随离中心位置的距离变化而变化,这里变化的是透明度alpha和字体大小TexSize,这两个值我都设置了Max和Min值,通过其与中心点的距离计算scale。我用的是变化曲线是抛物线scale=1-ax^2(x<=Height/4),scale = 0(x>Height/4),a=(4/Height)^2。x就是距离View中心的偏移量。用图片表示如下:

Android自定义控件实战——滚动选择器PickerView_第2张图片

难点2:

     text的居中。绘制text的时候不仅要使其在x方向上居中,还要在y方向上居中,在x方向上比较简单,设置Paint的Align为Align.CENTER就行了,但是y方向上很蛋疼,需要计算text的baseline。

难点3:

    循环滚动。为了解决循环滚动的问题我把存放text的List从中间往上下摊开,通过不断地moveHeadToTail和moveTailToHead使选中的text始终是list的中间position的值。

  

     以上就是几个难点,了解了之后可以来看PickerView的代码了:

[java]  view plain  copy
  1. package com.jingchen.timerpicker;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5. import java.util.Timer;  
  6. import java.util.TimerTask;  
  7.   
  8. import android.content.Context;  
  9. import android.graphics.Canvas;  
  10. import android.graphics.Paint;  
  11. import android.graphics.Paint.Align;  
  12. import android.graphics.Paint.FontMetricsInt;  
  13. import android.graphics.Paint.Style;  
  14. import android.os.Handler;  
  15. import android.os.Message;  
  16. import android.util.AttributeSet;  
  17. import android.view.MotionEvent;  
  18. import android.view.View;  
  19.   
  20. /** 
  21.  * 滚动选择器 
  22.  *  
  23.  * @author chenjing 
  24.  *  
  25.  */  
  26. public class PickerView extends View  
  27. {  
  28.   
  29.     public static final String TAG = "PickerView";  
  30.     /** 
  31.      * text之间间距和minTextSize之比 
  32.      */  
  33.     public static final float MARGIN_ALPHA = 2.8f;  
  34.     /** 
  35.      * 自动回滚到中间的速度 
  36.      */  
  37.     public static final float SPEED = 2;  
  38.   
  39.     private List<String> mDataList;  
  40.     /** 
  41.      * 选中的位置,这个位置是mDataList的中心位置,一直不变 
  42.      */  
  43.     private int mCurrentSelected;  
  44.     private Paint mPaint;  
  45.   
  46.     private float mMaxTextSize = 80;  
  47.     private float mMinTextSize = 40;  
  48.   
  49.     private float mMaxTextAlpha = 255;  
  50.     private float mMinTextAlpha = 120;  
  51.   
  52.     private int mColorText = 0x333333;  
  53.   
  54.     private int mViewHeight;  
  55.     private int mViewWidth;  
  56.   
  57.     private float mLastDownY;  
  58.     /** 
  59.      * 滑动的距离 
  60.      */  
  61.     private float mMoveLen = 0;  
  62.     private boolean isInit = false;  
  63.     private onSelectListener mSelectListener;  
  64.     private Timer timer;  
  65.     private MyTimerTask mTask;  
  66.   
  67.     Handler updateHandler = new Handler()  
  68.     {  
  69.   
  70.         @Override  
  71.         public void handleMessage(Message msg)  
  72.         {  
  73.             if (Math.abs(mMoveLen) < SPEED)  
  74.             {  
  75.                 mMoveLen = 0;  
  76.                 if (mTask != null)  
  77.                 {  
  78.                     mTask.cancel();  
  79.                     mTask = null;  
  80.                     performSelect();  
  81.                 }  
  82.             } else  
  83.                 // 这里mMoveLen / Math.abs(mMoveLen)是为了保有mMoveLen的正负号,以实现上滚或下滚  
  84.                 mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;  
  85.             invalidate();  
  86.         }  
  87.   
  88.     };  
  89.   
  90.     public PickerView(Context context)  
  91.     {  
  92.         super(context);  
  93.         init();  
  94.     }  
  95.   
  96.     public PickerView(Context context, AttributeSet attrs)  
  97.     {  
  98.         super(context, attrs);  
  99.         init();  
  100.     }  
  101.   
  102.     public void setOnSelectListener(onSelectListener listener)  
  103.     {  
  104.         mSelectListener = listener;  
  105.     }  
  106.   
  107.     private void performSelect()  
  108.     {  
  109.         if (mSelectListener != null)  
  110.             mSelectListener.onSelect(mDataList.get(mCurrentSelected));  
  111.     }  
  112.   
  113.     public void setData(List<String> datas)  
  114.     {  
  115.         mDataList = datas;  
  116.         mCurrentSelected = datas.size() / 2;  
  117.         invalidate();  
  118.     }  
  119.   
  120.     public void setSelected(int selected)  
  121.     {  
  122.         mCurrentSelected = selected;  
  123.     }  
  124.   
  125.     private void moveHeadToTail()  
  126.     {  
  127.         String head = mDataList.get(0);  
  128.         mDataList.remove(0);  
  129.         mDataList.add(head);  
  130.     }  
  131.   
  132.     private void moveTailToHead()  
  133.     {  
  134.         String tail = mDataList.get(mDataList.size() - 1);  
  135.         mDataList.remove(mDataList.size() - 1);  
  136.         mDataList.add(0, tail);  
  137.     }  
  138.   
  139.     @Override  
  140.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
  141.     {  
  142.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  143.         mViewHeight = getMeasuredHeight();  
  144.         mViewWidth = getMeasuredWidth();  
  145.         // 按照View的高度计算字体大小  
  146.         mMaxTextSize = mViewHeight / 4.0f;  
  147.         mMinTextSize = mMaxTextSize / 2f;  
  148.         isInit = true;  
  149.         invalidate();  
  150.     }  
  151.   
  152.     private void init()  
  153.     {  
  154.         timer = new Timer();  
  155.         mDataList = new ArrayList<String>();  
  156.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  157.         mPaint.setStyle(Style.FILL);  
  158.         mPaint.setTextAlign(Align.CENTER);  
  159.         mPaint.setColor(mColorText);  
  160.     }  
  161.   
  162.     @Override  
  163.     protected void onDraw(Canvas canvas)  
  164.     {  
  165.         super.onDraw(canvas);  
  166.         // 根据index绘制view  
  167.         if (isInit)  
  168.             drawData(canvas);  
  169.     }  
  170.   
  171.     private void drawData(Canvas canvas)  
  172.     {  
  173.         // 先绘制选中的text再往上往下绘制其余的text  
  174.         float scale = parabola(mViewHeight / 4.0f, mMoveLen);  
  175.         float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;  
  176.         mPaint.setTextSize(size);  
  177.         mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));  
  178.         // text居中绘制,注意baseline的计算才能达到居中,y值是text中心坐标  
  179.         float x = (float) (mViewWidth / 2.0);  
  180.         float y = (float) (mViewHeight / 2.0 + mMoveLen);  
  181.         FontMetricsInt fmi = mPaint.getFontMetricsInt();  
  182.         float baseline = (float) (y - (fmi.bottom / 2.0 + fmi.top / 2.0));  
  183.   
  184.         canvas.drawText(mDataList.get(mCurrentSelected), x, baseline, mPaint);  
  185.         // 绘制上方data  
  186.         for (int i = 1; (mCurrentSelected - i) >= 0; i++)  
  187.         {  
  188.             drawOtherText(canvas, i, -1);  
  189.         }  
  190.         // 绘制下方data  
  191.         for (int i = 1; (mCurrentSelected + i) < mDataList.size(); i++)  
  192.         {  
  193.             drawOtherText(canvas, i, 1);  
  194.         }  
  195.   
  196.     }  
  197.   
  198.     /** 
  199.      * @param canvas 
  200.      * @param position 
  201.      *            距离mCurrentSelected的差值 
  202.      * @param type 
  203.      *            1表示向下绘制,-1表示向上绘制 
  204.      */  
  205.     private void drawOtherText(Canvas canvas, int position, int type)  
  206.     {  
  207.         float d = (float) (MARGIN_ALPHA * mMinTextSize * position + type  
  208.                 * mMoveLen);  
  209.         float scale = parabola(mViewHeight / 4.0f, d);  
  210.         float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;  
  211.         mPaint.setTextSize(size);  
  212.         mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));  
  213.         float y = (float) (mViewHeight / 2.0 + type * d);  
  214.         FontMetricsInt fmi = mPaint.getFontMetricsInt();  
  215.         float baseline = (float) (y - (fmi.bottom / 2.0 + fmi.top / 2.0));  
  216.         canvas.drawText(mDataList.get(mCurrentSelected + type * position),  
  217.                 (float) (mViewWidth / 2.0), baseline, mPaint);  
  218.     }  
  219.   
  220.     /** 
  221.      * 抛物线 
  222.      *  
  223.      * @param zero 
  224.      *            零点坐标 
  225.      * @param x 
  226.      *            偏移量 
  227.      * @return scale 
  228.      */  
  229.     private float parabola(float zero, float x)  
  230.     {  
  231.         float f = (float) (1 - Math.pow(x / zero, 2));  
  232.         return f < 0 ? 0 : f;  
  233.     }  
  234.   
  235.     @Override  
  236.     public boolean onTouchEvent(MotionEvent event)  
  237.     {  
  238.         switch (event.getActionMasked())  
  239.         {  
  240.         case MotionEvent.ACTION_DOWN:  
  241.             doDown(event);  
  242.             break;  
  243.         case MotionEvent.ACTION_MOVE:  
  244.             doMove(event);  
  245.             break;  
  246.         case MotionEvent.ACTION_UP:  
  247.             doUp(event);  
  248.             break;  
  249.         }  
  250.         return true;  
  251.     }  
  252.   
  253.     private void doDown(MotionEvent event)  
  254.     {  
  255.         if (mTask != null)  
  256.         {  
  257.             mTask.cancel();  
  258.             mTask = null;  
  259.         }  
  260.         mLastDownY = event.getY();  
  261.     }  
  262.   
  263.     private void doMove(MotionEvent event)  
  264.     {  
  265.   
  266.         mMoveLen += (event.getY() - mLastDownY);  
  267.   
  268.         if (mMoveLen > MARGIN_ALPHA * mMinTextSize / 2)  
  269.         {  
  270.             // 往下滑超过离开距离  
  271.             moveTailToHead();  
  272.             mMoveLen = mMoveLen - MARGIN_ALPHA * mMinTextSize;  
  273.         } else if (mMoveLen < -MARGIN_ALPHA * mMinTextSize / 2)  
  274.         {  
  275.             // 往上滑超过离开距离  
  276.             moveHeadToTail();  
  277.             mMoveLen = mMoveLen + MARGIN_ALPHA * mMinTextSize;  
  278.         }  
  279.   
  280.         mLastDownY = event.getY();  
  281.         invalidate();  
  282.     }  
  283.   
  284.     private void doUp(MotionEvent event)  
  285.     {  
  286.         // 抬起手后mCurrentSelected的位置由当前位置move到中间选中位置  
  287.         if (Math.abs(mMoveLen) < 0.0001)  
  288.         {  
  289.             mMoveLen = 0;  
  290.             return;  
  291.         }  
  292.         if (mTask != null)  
  293.         {  
  294.             mTask.cancel();  
  295.             mTask = null;  
  296.         }  
  297.         mTask = new MyTimerTask(updateHandler);  
  298.         timer.schedule(mTask, 010);  
  299.     }  
  300.   
  301.     class MyTimerTask extends TimerTask  
  302.     {  
  303.         Handler handler;  
  304.   
  305.         public MyTimerTask(Handler handler)  
  306.         {  
  307.             this.handler = handler;  
  308.         }  
  309.   
  310.         @Override  
  311.         public void run()  
  312.         {  
  313.             handler.sendMessage(handler.obtainMessage());  
  314.         }  
  315.   
  316.     }  
  317.   
  318.     public interface onSelectListener  
  319.     {  
  320.         void onSelect(String text);  
  321.     }  
  322. }  


代码里的注释都写的很清楚了。接下来,我们就用写好的PickerView实现文章开头的图片效果吧~

首先看MainActivity的布局:

[html]  view plain  copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#000000" >  
  5.   
  6.     <RelativeLayout  
  7.         android:layout_width="wrap_content"  
  8.         android:layout_height="wrap_content"  
  9.         android:layout_centerInParent="true"  
  10.         android:background="#ffffff" >  
  11.   
  12.         <com.jingchen.timerpicker.PickerView  
  13.             android:id="@+id/minute_pv"  
  14.             android:layout_width="80dp"  
  15.             android:layout_height="160dp" />  
  16.   
  17.         <TextView  
  18.             android:id="@+id/minute_tv"  
  19.             android:layout_width="wrap_content"  
  20.             android:layout_height="wrap_content"  
  21.             android:layout_centerVertical="true"  
  22.             android:layout_toRightOf="@id/minute_pv"  
  23.             android:text="分"  
  24.             android:textColor="#ffaa33"  
  25.             android:textSize="26sp"  
  26.             android:textStyle="bold" />  
  27.   
  28.         <com.jingchen.timerpicker.PickerView  
  29.             android:id="@+id/second_pv"  
  30.             android:layout_width="80dp"  
  31.             android:layout_height="160dp"  
  32.             android:layout_toRightOf="@id/minute_tv" />  
  33.   
  34.         <TextView  
  35.             android:id="@+id/second_tv"  
  36.             android:layout_width="wrap_content"  
  37.             android:layout_height="wrap_content"  
  38.             android:layout_centerVertical="true"  
  39.             android:layout_toRightOf="@id/second_pv"  
  40.             android:text="秒"  
  41.             android:textColor="#ffaa33"  
  42.             android:textSize="26sp"  
  43.             android:textStyle="bold" />  
  44.     </RelativeLayout>  
  45.   
  46. </RelativeLayout>  
两个PickerView两个TextView,很简单。

下面是MainActivity的代码:

[java]  view plain  copy
  1. package com.jingchen.timerpicker;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import com.jingchen.timerpicker.PickerView.onSelectListener;  
  7.   
  8. import android.app.Activity;  
  9. import android.os.Bundle;  
  10. import android.view.Menu;  
  11. import android.widget.TextView;  
  12. import android.widget.Toast;  
  13.   
  14. public class MainActivity extends Activity  
  15. {  
  16.   
  17.     PickerView minute_pv;  
  18.     PickerView second_pv;  
  19.   
  20.     @Override  
  21.     protected void onCreate(Bundle savedInstanceState)  
  22.     {  
  23.         super.onCreate(savedInstanceState);  
  24.         setContentView(R.layout.activity_main);  
  25.         minute_pv = (PickerView) findViewById(R.id.minute_pv);  
  26.         second_pv = (PickerView) findViewById(R.id.second_pv);  
  27.         List<String> data = new ArrayList<String>();  
  28.         List<String> seconds = new ArrayList<String>();  
  29.         for (int i = 0; i < 10; i++)  
  30.         {  
  31.             data.add("0" + i);  
  32.         }  
  33.         for (int i = 0; i < 60; i++)  
  34.         {  
  35.             seconds.add(i < 10 ? "0" + i : "" + i);  
  36.         }  
  37.         minute_pv.setData(data);  
  38.         minute_pv.setOnSelectListener(new onSelectListener()  
  39.         {  
  40.   
  41.             @Override  
  42.             public void onSelect(String text)  
  43.             {  
  44.                 Toast.makeText(MainActivity.this"选择了 " + text + " 分",  
  45.                         Toast.LENGTH_SHORT).show();  
  46.             }  
  47.         });  
  48.         second_pv.setData(seconds);  
  49.         second_pv.setOnSelectListener(new onSelectListener()  
  50.         {  
  51.   
  52.             @Override  
  53.             public void onSelect(String text)  
  54.             {  
  55.                 Toast.makeText(MainActivity.this"选择了 " + text + " 秒",  
  56.                         Toast.LENGTH_SHORT).show();  
  57.             }  
  58.         });  
  59.     }  
  60.   
  61.     @Override  
  62.     public boolean onCreateOptionsMenu(Menu menu)  
  63.     {  
  64.         getMenuInflater().inflate(R.menu.main, menu);  
  65.         return true;  
  66.     }  
  67.   
  68. }  

你可能感兴趣的:(Android自定义控件实战——滚动选择器PickerView)