Android自定義控件_垂直滾動器PickerView

轉載請註明出處:http://blog.csdn.net/ljmingcom304/article/details/50393098
本文出自:【梁敬明的博客】

1.效果展示

  前段時間公司的產品設計了一款三級聯動的垂直滾動器PickerView,當時覺得挺簡單的,實際開發中還是遇到了點小障礙,於是上網上搜了段代碼用,代碼是別人寫的,直接Copy過來始終也是別人的東西,只有學到手才真正的屬於自己,於是稍微研究了下,並通過自己的方式實現。下面先上張效果圖,實現了其中一組數據的滾動選擇,其他兩組聯動數據的實現原理是一樣的,就是麻煩了點。
  

垂直滾動器

2.實現過程

  初始化畫筆和存放數據的集合。

private void init() {
    mDataList = new ArrayList<String>();
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Style.FILL);
    mPaint.setAntiAlias(true);
    mPaint.setTextAlign(Align.CENTER);
    mPaint.setColor(Color.BLACK);
}
   在效果圖中看以看到,當字體位於View中心時尺寸最大,位於兩側時尺寸最小。因此,需要設置字體的最大尺寸、最小尺寸以及字體間的距離。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mViewHeight = getMeasuredHeight();
    mViewWidth = getMeasuredWidth();
    // 按照View的高度計算字體大小
    mMaxTextSize = mViewHeight / 6.0f;
    mMinTextSize = mMaxTextSize / 1.5f;
    mDistance = MARGIN_ALPHA * mViewHeight;
}
   初始化默認繪製,首先將當前選中的元素繪製到View的中心位置,索引在當前元素前的繪製到當前元素的上方,索引在當前元素後的繪製到當前元素的下方。
/**
 * @param position:當前元素前後第幾個位置,若爲當前元素position爲0
 * @param direction:繪製的元素相對於當前元素的方向,上方爲-1,下方爲1,當前元素默認爲下方
 */
private void drawText(Canvas canvas, int position, int direction) {
    // 元素距離控件中心的相對距離
    float offset = (float) (mDistance * position + direction * mMoveLen);

    // 縮放倍數:位於控件中心時,縮放倍數是1;控件中心的前一個數據和後一個數據的縮放倍數是0
    float f = (float) (1 - Math.pow(offset / mDistance, 2));// 按拋物線縮放
    // 當數據與控件中心的距離,大於數據間的初始距離時,不再進行縮放
    float scale = f < 0 ? 0 : f;

    // 字體尺寸
    float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;

    // 字體大小與透明度
    mPaint.setTextSize(size);
    mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));

    float x = (float) (mViewWidth / 2.0);
    float y = getTextBaseLine((float) (mViewHeight / 2.0 + direction
            * offset));

    canvas.drawText(mDataList.get(mCurrentSelected + direction * position),
            x, y, mPaint);
}
  處理View的觸摸滑動事件,包括觸摸按下事件、觸摸滑動事件和觸摸離開事件。通過效果圖可以知道,在字體沒有滑動到初始化位置時手指離開,字體會自動滑動到初始化的位置,這裏通過定時器的方式實現字體的自動滑動。   當手指按下時,首先要初始化定時任務,將上次的定時任務清除,並記錄下手指按下的位置。
private void doDown(MotionEvent event) {
    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }
    mLastDownY = event.getY();
}
   當手指進行滑動時,獲取每次滑動的距離,當滑動的距離超過字體間相對距離的一半時,調整字體在集合中的位置。向上滑動時,第一個元素移動到最後位置,其餘元素整體前移,將每個元素的滑動距離整體向下移動一個相對位置。向上滑動時,最後一個元素移動到集合的首位,其餘元素整體後移,將每個元素的滑動距離整體向上移動一個相對位置。
private void doMove(MotionEvent event) {

    mMoveLen += (event.getY() - mLastDownY);

    // 當向下滑動時,mMoveLen爲正;當向上滑動時,mMoveLen爲負
    if (mMoveLen > mDistance / 2) {
        // 往下滑超過離開距離
        String tail = mDataList.get(mDataList.size() - 1);
        mDataList.remove(mDataList.size() - 1);
        mDataList.add(0, tail);

        // 重新設置數據的位置,將其整體上移
        mMoveLen -= mDistance;
    } else if (mMoveLen < -mDistance / 2) {
        // 往上滑超過離開距離
        String head = mDataList.get(0);
        mDataList.remove(0);
        mDataList.add(head);

        // 重新設置數據的位置,將其整體下移
        mMoveLen += mDistance;
    }

    mLastDownY = event.getY();
    invalidate();
}

  當手指離開時,開啓定時任務,保證字體的初始化相對View的位置不發生改變。

private void doUp(MotionEvent event) {
    // 擡起手後mCurrentSelected的位置由當前位置move到中間選中位置
    if (Math.abs(mMoveLen) < 0.0001) {
        mMoveLen = 0;
        return;
    }

    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }

    mHandler.postDelayed(mRunnable, 10);
}

  開啓定時任務,判斷當前字體已經滑動的距離,每隔指定時間自動滑動指定的距離,直到字體滑動到初始化相對View的位置。

private Runnable mRunnable = new Runnable() {

    @Override
    public void run() {
        if (Math.abs(mMoveLen) < SPEED) {
            mMoveLen = 0;
            if (mHandler != null) {
                mHandler.removeCallbacks(this);
                // 事件監聽
                if (mSelectListener != null)
                    mSelectListener.onSelect(mDataList
                            .get(mCurrentSelected));
            }
        } else{
            // 這裏mMoveLen / Math.abs(mMoveLen)是爲了保有mMoveLen的正負號,以實現上滾或下滾
            // 用於將數據回滾到起始位置或者終點位置
            mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;
            mHandler.postDelayed(this, 10);
        }
        invalidate();
    }
};

3.示例代碼

  下面上完整代碼,首先是佈局文件。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray" >

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="#ffffff" >

        <com.example.pickerview.PickerView
            android:id="@+id/minute_pv"
            android:layout_width="160dp"
            android:layout_height="320dp" />

    </RelativeLayout>

</RelativeLayout>

  然後是Activity。

public class MainActivity extends Activity {

    PickerView pv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        pv = (PickerView) findViewById(R.id.minute_pv);
        List<String> data = new ArrayList<String>();
        for (int i = 0; i < 10; i++) {
            data.add("0" + i);
        }
        pv.setData(data);
        pv.setOnSelectListener(new onSelectListener() {

            @Override
            public void onSelect(String text) {
                Toast.makeText(MainActivity.this, "選擇" + text + " 分",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

}

  最後是實現代碼。

public class PickerView extends View {

/**
 * text之間間距和minTextSize之比,手工進行配置
 */
public static final float MARGIN_ALPHA = 0.35f;
/**
 * 自動回滾到中間的速度
 */
public static final float SPEED = 2;

private List&lt;String&gt; mDataList;
/**
 * 選中的位置,這個位置是mDataList的中心位置,一直不變
 */
private int mCurrentSelected;
private Paint mPaint;

private float mMaxTextSize;
private float mMinTextSize;

private float mMaxTextAlpha = 255;
private float mMinTextAlpha = 120;

// 要繪製的數據位於當前數據的上方或下方
private int mUp = -1;
private int mDown = 1;
/** 相鄰數據間的距離 */
private float mDistance;

private int mViewHeight;
private int mViewWidth;

private float mLastDownY;
/** 滑動的距離 */
private float mMoveLen = 0;
private Handler mHandler = new Handler();
private onSelectListener mSelectListener;

public PickerView(Context context) {
    this(context, null);
}

public PickerView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public PickerView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
}

interface onSelectListener {
    void onSelect(String text);
}

public void setOnSelectListener(onSelectListener listener) {
    mSelectListener = listener;
}

/** 添加數據 */
public void setData(List&lt;String&gt; datas) {
    mDataList = datas;
    mCurrentSelected = datas.size() / 2;
    invalidate();
}

public void setSelected(int selected) {
    mCurrentSelected = selected;
}

private void init() {
    mDataList = new ArrayList&lt;String&gt;();

    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Style.FILL);
    mPaint.setAntiAlias(true);
    mPaint.setTextAlign(Align.CENTER);
    mPaint.setColor(Color.BLACK);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mViewHeight = getMeasuredHeight();
    mViewWidth = getMeasuredWidth();
    // 按照View的高度計算字體大小
    mMaxTextSize = mViewHeight / 6.0f;
    mMinTextSize = mMaxTextSize / 1.5f;
    mDistance = MARGIN_ALPHA * mViewHeight;
}

@Override
protected void onDraw(Canvas canvas) {
    // 繪製當前元素
    drawText(canvas, 0, mDown);
    // 繪製上方元素
    for (int i = 1; (mCurrentSelected - i) &gt;= 0; i++) {
        drawText(canvas, i, mUp);
    }
    // 繪製下方元素
    for (int i = 1; (mCurrentSelected + i) &lt; mDataList.size(); i++) {
        drawText(canvas, i, mDown);
    }
}

/**
 * @param position:當前元素前後第幾個位置,若爲當前元素position爲0
 * @param direction:繪製的元素相對於當前元素的方向,上方爲-1,下方爲1,當前元素默認爲下方
 */
private void drawText(Canvas canvas, int position, int direction) {
    // 元素距離控件中心的相對距離
    float offset = (float) (mDistance * position + direction * mMoveLen);

    // 縮放倍數:位於控件中心時,縮放倍數是1;控件中心的前一個數據和後一個數據的縮放倍數是0
    float f = (float) (1 - Math.pow(offset / mDistance, 2));// 按拋物線縮放
    // 當數據與控件中心的距離,大於數據間的初始距離時,不再進行縮放
    float scale = f &lt; 0 ? 0 : f;

    // 字體尺寸
    float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;

    // 字體大小與透明度
    mPaint.setTextSize(size);
    mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));

    float x = (float) (mViewWidth / 2.0);
    float y = getTextBaseLine((float) (mViewHeight / 2.0 + direction
            * offset));

    canvas.drawText(mDataList.get(mCurrentSelected + direction * position),
            x, y, mPaint);
}

// 調整字體的位置,使其垂直居中進行繪製
private float getTextBaseLine(float p) {
    FontMetricsInt fmi = mPaint.getFontMetricsInt();
    float baseline = (float) (p - (fmi.bottom / 2.0 + fmi.top / 2.0));
    return baseline;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
        doDown(event);
        break;
    case MotionEvent.ACTION_MOVE:
        doMove(event);
        break;
    case MotionEvent.ACTION_UP:
        doUp(event);
        break;
    }
    return true;
}

private void doDown(MotionEvent event) {
    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }

    mLastDownY = event.getY();
}

private void doMove(MotionEvent event) {

    mMoveLen += (event.getY() - mLastDownY);

    // 當向下滑動時,mMoveLen爲正;當向上滑動時,mMoveLen爲負
    if (mMoveLen &gt; mDistance / 2) {
        // 往下滑超過離開距離
        String tail = mDataList.get(mDataList.size() - 1);
        mDataList.remove(mDataList.size() - 1);
        mDataList.add(0, tail);

        // 重新設置數據的位置,將其整體上移
        mMoveLen -= mDistance;
    } else if (mMoveLen &lt; -mDistance / 2) {
        // 往上滑超過離開距離
        String head = mDataList.get(0);
        mDataList.remove(0);
        mDataList.add(head);

        // 重新設置數據的位置,將其整體下移
        mMoveLen += mDistance;
    }

    mLastDownY = event.getY();
    invalidate();
}

private void doUp(MotionEvent event) {
    // 擡起手後mCurrentSelected的位置由當前位置move到中間選中位置
    if (Math.abs(mMoveLen) &lt; 0.0001) {
        mMoveLen = 0;
        return;
    }

    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }

    mHandler.postDelayed(mRunnable, 10);
}

private Runnable mRunnable = new Runnable() {

    @Override
    public void run() {
        if (Math.abs(mMoveLen) &lt; SPEED) {
            mMoveLen = 0;
            if (mHandler != null) {
                mHandler.removeCallbacks(this);
                // 事件監聽
                if (mSelectListener != null)
                    mSelectListener.onSelect(mDataList
                            .get(mCurrentSelected));
            }
        } else{
            // 這裏mMoveLen / Math.abs(mMoveLen)是爲了保有mMoveLen的正負號,以實現上滾或下滾
            // 用於將數據回滾到起始位置或者終點位置
            mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;
            mHandler.postDelayed(this, 10);
        }
        invalidate();
    }
};

}

  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章