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

时间:2022-08-30 20:05:14

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


今日科技快讯


据美国财经网站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 defaultHeight=0, childHeight=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


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

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

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

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