绚丽的自定义星期选择控件
今日科技快讯
据美国财经网站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
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
- 沪交警严查整治机动车非法改装 一个星期查获1172起
- 12月16日(星期六)骑行西山后山
- 【股票早餐】2017年12月14日星期四(附股)
- 联讯早参20171214星期四
- 【财经早餐粤语版】2017.12.14星期四
- 【财经早餐】2017.12.14星期四
- 陆家嘴财经早餐2017年12月14日星期四
- 【格上财经早餐】12月14日(星期四)
- 麦肯12月13日(星期三)团体课程分享
- 活动策划好,流量滚滚来