绚丽的自定义星期选择控件

绚丽的自定义星期选择控件


今日科技快讯

据美国财经网站IPOScoop报道,搜狗将于美国东部时间11月6日在纽交所挂牌交易。该报道称,搜狗将以每股11美元至13美元的价格发行4500万股美国存托股票(ADS),拟融资约5.4亿美元。10月27日,搜狗对招股书进行了更新,将IPO发行价区间定为每股美国存托股票11美元至13美元,最高融资6.6275亿美元。

作者简介

本篇来自 游资程序员 的投稿,分享了炫丽横向滚动选择星期控件,希望能够给大家带来帮助。

游资程序员 的博客地址:

http://blog.csdn.net/hzmming2008

开始

最近项目需要一个横向选择的星期的控件,需求如下: 

  • 当用户点击某个星期时能自动的选中这个星期的时期,并且选中后能放大以区分未选中的。 

  • 点击后能够自动滚动到屏幕中间的位置。 

  • 当用户滑动时不改变选中状态,但是滑动时时期跟随手指移动。 

  • 效果图如下:

    绚丽的自定义星期选择控件

    分析

    从上图的效果看,很明显现有的控件无法满足,因此需要自定义 view。 

    从上图的效果看都是文字并且要可以点击和滑动,所以自然的想到可以用 textview 加载到 viewgroup 中去,然后重写 viewgroup的onTouchEvent 方法来实现滑动。 

    其次可以重写 viewgroup 的 onLayout 的方法来实现横向排列 textView。另外 textView 还需要有点击事件,因此必然需要重写 viewgroup 的 onInterceptTouchEvent 方法,来解决滑动冲突。下面是一步一步的去实现它。

    控件初始化

    因为控件需要滑动,我们在初始化时一并初始化定义的 mTouchSlop,它是用来来获取手机滑动的最小值,只有大于他时才认为是滑动。另外初始化一个 Scroller,用来实现如上面效果图的平滑滚动。

    OnWeekClickListener mListener;//用户点击时的回调接口

    private Scroller mScroller;//用于完成滚动操作的实例

    private int mTouchSlop; //判定为拖动的最小移动像素数

    List<NodeInterFace> mDatas = new ArrayList<>();//显示的数据

    int selectIndex=2;//默认选中第三个

    int itemWidth; private int view_margin_left_or_right;//view两边的的margin



    private int leftBorder;//左边界



    private int rightBorder;//右边界



    public Week(Context context) {    this(context,null); } public Week(Context context, AttributeSet attrs) {    this(context, attrs,0); } public Week(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    // 第一步,创建Scroller的实例    mScroller = new Scroller(context);    ViewConfiguration configuration = ViewConfiguration.get(context);    // 获取TouchSlop值    mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); }

    重写onMeasure

    由于本控件宽度是填充父窗体,高度是包裹内容,所以我们只需重新设置一下高度即可,而高度只需要随便获取一个 textview 的高度即可,另外为了让他有 padding 的效果,我们设置 viewgoup 的高度为 textView 的1.5倍,代码如下:

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    measureChildren(widthMeasureSpec,heightMeasureSpec);    int defaultchildHeight=0;    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);    if(heightSpecMode == MeasureSpec.AT_MOST){//高设置为wrap_content        for (int i=0;i<getChildCount();i++){ //由于只有一行  所以随便取一个的高度即可            childHeight= (int) (getChildAt(0).getMeasuredHeight()*1.5);        }        setMeasuredDimension(widthSpecSize,childHeight); //无论用户将宽设置为何种模式  都与match_parent相同    } else{

           //宽高都设置为match_parenth或具体的dp值        setMeasuredDimension(widthSpecSize, heightSpecSize);    } }

    重写onLayout

    从效果图可以看到选中的 textview 是居中且放大的,另外两边都是两个 textview,所以一屏就是5个 view,每个 textView 的宽度就是 itemWidth=getWidth()/5,另外他的view_margin_left_or_right 就相当于是 textview 的左右的 margin, view_margin_left_or_right=(itemWidth-getChildAt(0).getMeasuredWidth())/2。为了实现居中效果,现将选中的 textView 居中放置,再依次布局左右两边的textView的位置。另外需要在布置完成后,初始化左右边界即 leftBorder,rightBorder。这是用来在 onTouchEvent 方法中检查是否滑出边界。代码如下:

    @Override

    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {    itemWidth=getWidth()/5; //每行显示五个    int right,bottom,left;    bottom=getHeight()-(getHeight()-getChildAt(0).getMeasuredHeight())/2;    //itemWidth的宽度一定是大于 实际的每个子view的宽度的    view_margin_left_or_right=(itemWidth-getChildAt(0).getMeasuredWidth())/2;//相当于左右边距    int left_X=getWidth()-itemWidth*3;//中间一个的左边界坐标    for (int j=selectIndex-1;j >= 0;j--){ //中间那个view左边的那些view        View view=getChildAt(j);        view.setScaleX(1.0f);        view.setScaleY(1.0f);        right=left_X-view_margin_left_or_right-itemWidth*(selectIndex-1-j);        view.layout(right-getChildAt(j).getMeasuredWidth(),bottom-getChildAt(j).getMeasuredHeight(),right,bottom);    }    int right_X=itemWidth*3;;//中间一个的右边界坐标    for (int m=selectIndex+1;m<getChildCount();m++){ //中间那个view右边的那些view        View view=getChildAt(m);        view.setScaleX(1.0f);        view.setScaleY(1.0f);        left=right_X+view_margin_left_or_right+itemWidth*(m-(selectIndex+1));        view.layout(left,bottom-getChildAt(m).getMeasuredHeight(),left+getChildAt(m).getMeasuredWidth(),bottom);    }    //中间一个view    left=itemWidth*2+view_margin_left_or_right;    getChildAt(selectIndex).layout(left,bottom-getChildAt(selectIndex).getMeasuredHeight(),left+getChildAt(selectIndex).getMeasuredWidth(),bottom);    getChildAt(selectIndex).setScaleX(1.2f);    getChildAt(selectIndex).setScaleY(1.2f);    // 初始化左右边界值    leftBorder = getChildAt(0).getLeft();    rightBorder = getChildAt(getChildCount() - 1).getRight(); }

    重写onInterceptTouchEvent

    在这通过 mTouchSlop 来判断用户是滑动还是点击,只有大于 mTouchSlop 才认为是滑动,当是滑动时返回 true 对事件拦截掉,不让其传到textView以便调用 viewgroup 的onTouchEvent 来进行滑动。代码如下:

    /** * 手机按下时的屏幕坐标 */

    private float mXDown; /** * 手机当时所处的屏幕坐标 */

    private float mXMove; /** * 上次触发ACTION_MOVE事件时的屏幕坐标 */

    private float mXLastMove; @Override

    public boolean onInterceptTouchEvent(MotionEvent ev) {    switch (ev.getAction()) {        case MotionEvent.ACTION_DOWN:            mXDown = ev.getRawX();            mXLastMove = mXDown;            break;        case MotionEvent.ACTION_MOVE:            mXMove = ev.getRawX();            float dx=Math.abs(mXMove-mXDown);            if (dx>mTouchSlop){                return true;            }            break;    }    return super.onInterceptTouchEvent(ev); }

    重写onTouchEvent

    在 ACTION_MOVE 事件中计算用户手指滑动的距离,并通过 scrollBy 来滑动响应距离。注意判断是否滑出了屏幕的边界,滑出边界就调用scrollTo回到边界,否则调用scrollBy(scrolledX, 0) 滑动相应的距离,里面得注释很详细了,就不多说了。

    @Override

    public boolean onTouchEvent(MotionEvent event) {    switch (event.getAction()){        case MotionEvent.ACTION_DOWN:            break;        case MotionEvent.ACTION_MOVE:            mXMove = event.getRawX();

               //计算本次view的移动距离            int scrolledX = (int) (mXLastMove - mXMove);            if (getScrollX() + scrolledX < leftBorder) {

                   //以前滑动的距离加上本次滑动的距离比左边第一个view的lfet小 既为滑出左边界了 滑出左边界是向右滑动所以getScrollX()为负值                scrollTo(leftBorder-view_margin_left_or_right, 0);                return true;            } else if (getScrollX() + scrolledX > rightBorder-getWidth()) {

                   //以前滑动的距离加上本次滑动的距离比右边最后一个view的right减去viewGroup的宽度大 既为滑出右边界了 滑出右边界是向左滑动所以getScrollX()为正值                scrollTo(rightBorder+view_margin_left_or_right - getWidth(), 0);                return true;            }            scrollBy(scrolledX, 0);            mXLastMove = mXMove;            break;        case MotionEvent.ACTION_UP:            break;    }    return true; }

    数据的加载与显示

    我们在这里通过setData方法把链表中的数据显示到textView上,并将链表的位置信息放在textView的tag中,用来判断用户点击的位置,最后通过viewgroup的addview方法将textView加载到viewgroup中来。

    public void setData(List<NodeInterFace>  mList,  OnWeekClickListener listener){    mListener=listener;    mDatas=mList;    if (mDatas!=null){        for (int i=0;i<mDatas.size();i++){            TextView tv=(TextView) LayoutInflater.from(getContext()).inflate(R.layout.item_view,this,false);            tv.setText(mDatas.get(i).getDate());            tv.setTag(i);            tv.setOnClickListener(new OnClickListener() {                @Override                public void onClick(View view) {                    int pos=(int)view.getTag();                    startAnim(pos, selectIndex);                    selectIndex=pos;                    Log.e("pos:",pos+"");                    mListener.onClick( mDatas.get(pos).getDate());                }            });            if (i>1 && mDatas.get(i).isSelected() && i< mDatas.size()-2 ){                selectIndex=i;            }            addView(tv);        }    } }

    下面是 R.layout.item_view 的布局:

    <?xml version="1.0" encoding="utf-8"?>

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"

       android:layout_width="wrap_content"

       android:layout_height="wrap_content"

       android:textColor="#fff"

       android:textSize="10sp"

       android:gravity="center"

       android:background="@drawable/tv_bg" />

    下面是 textview 的圆形背景:

    <?xml version="1.0" encoding="utf-8"?>

    <shape xmlns:android="http://schemas.android.com/apk/res/android"

       android:shape="oval"

       android:useLevel="false">

       <!-- 实心 -->

       <solid  android:color="#383" />

       <!-- 圆角 -->

       <corners android:radius="360dp" />



       <!-- 边距 -->

       <padding        android:bottom="1dp"        android:left="1dp"        android:right="1dp"        android:top="1dp" />

       <!-- 大小 -->

       <size android:width="50dp"        android:height="50dp" />

    </shape>

    用户点击后开始滚动的动画

    上面你看到了用户点击后调用了startAnim方法来实现滚动和点击后的textView的放大与缩小,当用户点击后我们首先通过ObjectAnimator对当前选中的textView进行放大,并对之前选中的textView进行缩小,再计算当前点击的textView需要滚动多少距离才能滚动的屏幕的中间,最后通过mScroller来进行平滑的滚动,代码如下:

    private void startAnim(int current, int last){    if (current==last) return;    ObjectAnimator anim1_current = ObjectAnimator.ofFloat(getChildAt(current), "scaleX", 1.0f, 1.2f);    ObjectAnimator anim2_current = ObjectAnimator.ofFloat(getChildAt(current), "scaleY", 1.0f, 1.2f);    ObjectAnimator anim1_last = ObjectAnimator.ofFloat(getChildAt(last), "scaleX", 1.2f, 1.0f);    ObjectAnimator anim2_last = ObjectAnimator.ofFloat(getChildAt(last), "scaleY", 1.2f, 1.0f);    AnimatorSet set=new AnimatorSet();    set.setDuration(500);    set.playTogether(anim1_current,anim2_current,anim1_last,anim2_last);    set.start();   int dx= getDeletaX( current,  last);    if (getScrollX() + dx < leftBorder) { //以前滑动的距离加上本次滑动的距离比左边第一个view的lfet小 既为滑出左边界了 滑出左边界是向右滑动所以getScrollX()为负值        scrollTo(leftBorder-view_margin_left_or_right, 0);        return ;    } else if (getScrollX() + dx > rightBorder-getWidth()) {//以前滑动的距离加上本次滑动的距离比右边最后一个view的right减去viewGroup的宽度大 既为滑出右边界了 滑出右边界是向左滑动所以getScrollX()为正值        scrollTo(rightBorder+view_margin_left_or_right - getWidth(), 0);        return ;    }    //        scrollBy(dx,0);    mScroller.startScroll(getScrollX(), 0, dx, 0,500);    invalidate(); }

    平滑滚动需要重写 computeScroll:

    public void computeScroll() {    // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑    if (mScroller.computeScrollOffset()) {        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());        invalidate();    } }

    到此所有的工作就完成了,喜欢请点赞,谢谢。

    源码地址如下:

    http://download.csdn.net/download/hzmming2008/10025587

    欢迎长按下图 -> 识别图中二维码

    或者 扫一扫 关注我的公众号

    绚丽的自定义星期选择控件

    绚丽的自定义星期选择控件