Android自定义View(RollWeekView-炫酷的星期日期选择控件)
一次展示一个星期的5天,中间放大的为当前选中的;如果点击了其中一个日期,比如星期五,那么整体向左滑动,并将星期五慢慢放大,星期三慢慢缩小,星期六和星期天就展示出来了;如果继续点击星期六,那么照样是整体向左滑动一个单位,星期日右边就需要塞进一个星期一。就是一个无限循环的过程,中间选中的需要展示当天的日期。
1、分析
??最开始,我想到在LinearLayout中依次排开7个子控件(星期几),当点击之后,把所有的子控件通过动画移动相应的偏移量,但是,如果移动后就会出现空位的情况(周一的左边和周日的右边没有了),所以这种方式可是可以,但是要解决无限循环空位的情况。
??于是就想到能不能多添加几个子控件作为空位替补?这个方法肯定能实现,但是究竟需要多少个替补呢?这些替补应该放在什么位置呢?
??当前展示的5个子控件中,如果点击最左边或者最右边,这样的偏移量是最大的,需要便宜两个子控件单位,所以左边和右边最多个需要2个替补就行了,算起来也不多,一共才9个(当前展示5个,左边隐藏替补2个,右边隐藏替补2个)。在点击之前,先将替补位置设置好,当点击之后,所有子控件执行偏移动画(不可能出现空位,最多偏移2位),动画结束之后,再重新为隐藏的4个替补分配位置并绑定数据,绑定什么数据那就看当前中间显示的是星期几了。
分析图如下:
但是新的问题又来了,动画执行完毕之后,怎么判断哪些子控件是当前正在显示的呢(中间五个)?由于正在显示的子控件的位置是可以确定的,因为要重写onMeasure(),在onMeasure()方法中可以得到单个子控件的宽度(item_width=getMeasureWidth()/5),动画结束之后,遍历这些子控件,如果x==0的那肯定就是当前正在展示的第一个子控件,x==item_width则是第二个….,剩余的4个就不用判断了,直接将两个移动到左边,两个移动到右边,然后重新为这四个替补设置数据。
2、定义控件布局
组合控件的布局如下:
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical">
android:id="@+id/ll_1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_3"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_4"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_5"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_6"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_7"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_8"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/ll_9"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center">
android:layout_width="@dimen/week_item_sise"
android:layout_height="@dimen/week_item_sise"
android:background="@drawable/week_one_bg"
android:orientation="vertical"
android:gravity="center">
android:id="@+id/tv_9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
android:id="@+id/tv_date9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="周"/>
布局写完后,接下来应该找一个容器接纳他们,这个容器就是我们定义的CustomWeekView,让其继承LinearLayout,然后重写构造方法,并初始化子控件;此处自定义属性就不再赘述,之前的所有自定义控件案例中都讲解过
publicclassCustomWeekViewextendsLinearLayout{
publicCustomWeekView(Contextcontext){
this(context,null);
}
publicCustomWeekView(Contextcontext,AttributeSetattrs){
this(context,attrs,0);
}
publicCustomWeekView(Contextcontext,AttributeSetattrs,intdefStyleAttr){
super(context,attrs,defStyleAttr);
init(context,attrs);
}
privatevoidinit(Contextcontext,AttributeSetattrs){
LayoutInflater.from(context).inflate(R.layout.custom_week_layout,this,true);
ll_1=(LinearLayout)findViewById(R.id.ll_1);
ll_2=(LinearLayout)findViewById(R.id.ll_2);
...
tv_1=(TextView)findViewById(R.id.tv_1);
tv_2=(TextView)findViewById(R.id.tv_2);
...
tv_date1=(TextView)findViewById(R.id.tv_date1);
tv_date2=(TextView)findViewById(R.id.tv_date2);
...
if(attrs!=null){
TypedArrayta=context.obtainStyledAttributes(attrs,R.styleable.LimitScroller);
limit=5;
background=ta.getColor(R.styleable.LimitScroller_android_background,Color.TRANSPARENT);
textSize=ta.getDimension(R.styleable.LimitScroller_android_textSize,15f);
finalfloatfontScale=context.getResources().getDisplayMetrics().scaledDensity;
textSize=textSize/fontScale+0.5f;
dateTextSize=ta.getInteger(R.styleable.LimitScroller_dateTextSize,12);
textColor=ta.getColor(R.styleable.LimitScroller_android_textColor,Color.BLACK);
dateTextColor=ta.getColor(R.styleable.LimitScroller_dateTextColor,Color.RED);
durationTime=ta.getInt(R.styleable.LimitScroller_durationTime,1000);
scaleSize=ta.getFloat(R.styleable.LimitScroller_scaleSize,0);
ta.recycle();//注意回收
}
}
}
4、重写onMeasure
??由于本控件宽度是填充父窗体,高度是包裹内容,这里不需要我们手动写代码测量,直接调用super.onMeasure()好了。。但是我们需要在测量完毕后,计算子控件的宽度并初次安置每个子控件的位置,这个宽度就是后面移动的单位,也作为位置判断的依据。由于onMeasure()方法会被调用多次(后面设置位置、显示隐藏等等操作都会导致onMeasure()触发),所以需要判断一下如果ITEM_WIDTH<=0才需要计算;然后还要为子控件设置初始日期:
@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
//Log.w(TAG,"测量完成,宽度高度="+getMeasuredWidth()+""+getMeasuredHeight());
if(ITEM_WIDTH<=0){
ITEM_WIDTH=getMeasuredWidth()/limit;
//Log.w(TAG,"每小项尺寸:"+ITEM_WIDTH+""+getMeasuredHeight());
measureInit();
}
if(ll_2.getX()>0&&!isFirstSeted){
//设置今天的日期在中间
animalFinish=false;
Calendarcal=Calendar.getInstance();
inttodayNum=cal.get(Calendar.DAY_OF_WEEK)-1;
Log.d(TAG,"今天是星期"+WEEK_STR[todayNum]);
setCenter(getEnumByNum(todayNum));
animalFinish=true;
isFirstSeted=true;
}
}
/根据屏幕的宽度和显示的个数,设置item的宽度/
privatevoidmeasureInit(){
LinearLayout.LayoutParamslp=(LinearLayout.LayoutParams)ll_1.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_1.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_2.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_2.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_3.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_3.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_4.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_4.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_5.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_5.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_6.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_6.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_7.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_7.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_8.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_8.setLayoutParams(lp);
lp=(LinearLayout.LayoutParams)ll_9.getLayoutParams();
lp.width=ITEM_WIDTH;
ll_9.setLayoutParams(lp);
}
5、点击后执行动画
??上面的步骤完成之后,基本上已经完成的控件的初始设置,接下来就是点击子控件后执行动画了。这里一共需要执行两套动画,一套是缩放动画、一套是移动动画。添加点击事件的代码就不贴出来了,下面是执行动画的方法:
privatevoidstartAnimation(finalWEEKDAYcenterWitch,finalLinearLayoutllClickView){
if(centerWitch==centerNow)
return;
animalFinish=false;
LinearLayoutinnerLL=(LinearLayout)ll3.getChildAt(0);
TextViewtvDate=(TextView)innerLL.getChildAt(1);
tvDate.setVisibility(View.GONE);
innerLL=(LinearLayout)llClickView.getChildAt(0);
tvDate=(TextView)innerLL.getChildAt(1);
tvDate.setVisibility(View.VISIBLE);
//根据当前中间位置显示的和被点击的日期,获取需要偏移的增量
intoffset=getXOffset(centerWitch);
Log.d(TAG,"当前中间为"+centerNow+",点击的是"+centerWitch+"偏移量:"+offset);
//当前中间位置的需要缩放到原尺寸
//Log.v(TAG,"中间item缩放量scaleX="+ll3.getChildAt(0).getScaleX()+"scaleY="+ll3.getChildAt(0).getScaleY());
ObjectAnimatoranim100=ObjectAnimator.ofFloat(ll3.getChildAt(0),"scaleX",ll3.getChildAt(0).getScaleX(),1.0f);
ObjectAnimatoranim101=ObjectAnimator.ofFloat(ll3.getChildAt(0),"scaleY",ll3.getChildAt(0).getScaleY(),1.0f);
//被点击的需要放大
ObjectAnimatoranim102=ObjectAnimator.ofFloat(llClickView.getChildAt(0),"scaleX",1,scaleSize);
ObjectAnimatoranim103=ObjectAnimator.ofFloat(llClickView.getChildAt(0),"scaleY",1,scaleSize);
//透明度动画
ObjectAnimatoranim104=ObjectAnimator.ofFloat(llClickView.getChildAt(0),"scaleY",1,scaleSize);
//位移动画
ObjectAnimatoranim1=ObjectAnimator.ofFloat(ll_1,"X",ll_1.getX(),ll_1.getX()+offset);
ObjectAnimatoranim2=ObjectAnimator.ofFloat(ll_2,"X",ll_2.getX(),ll_2.getX()+offset);
ObjectAnimatoranim3=ObjectAnimator.ofFloat(ll_3,"X",ll_3.getX(),ll_3.getX()+offset);
ObjectAnimatoranim4=ObjectAnimator.ofFloat(ll_4,"X",ll_4.getX(),ll_4.getX()+offset);
ObjectAnimatoranim5=ObjectAnimator.ofFloat(ll_5,"X",ll_5.getX(),ll_5.getX()+offset);
ObjectAnimatoranim6=ObjectAnimator.ofFloat(ll_6,"X",ll_6.getX(),ll_6.getX()+offset);
ObjectAnimatoranim7=ObjectAnimator.ofFloat(ll_7,"X",ll_7.getX(),ll_7.getX()+offset);
ObjectAnimatoranim8=ObjectAnimator.ofFloat(ll_8,"X",ll_8.getX(),ll_8.getX()+offset);
ObjectAnimatoranim9=ObjectAnimator.ofFloat(ll_9,"X",ll_9.getX(),ll_9.getX()+offset);
AnimatorSetanimSet=newAnimatorSet();
animSet.setDuration(duwww.shanxiwang.netrationTime);
animSet.playTogether(anim100,anim101,anim102,anim103,anim1,anim2,anim3,anim4,anim5,anim6,anim7,anim8,anim9);
animSet.addListener(newAnimator.AnimatorListener(){
@Override
publicvoidonAnimationStart(Animatoranimation){
}
@Override
publicvoidonAnimationEnd(Animatoranimation){
Log.w(TAG,"动画结束后位置:"+ll_1.getX()+""+ll_2.getX()+""+ll_3.getX()+""
+ll_4.getX()+""+ll_5.getX()+""+ll_6.getX()
+""+ll_7.getX()+""+ll_8.getX()+""+ll_9.getX());
setCenter(centerWitch);
animalFinish=true;
}
@Override
publicvoidonAnimationCancel(Animatoranimation){
}
@Override
publicvoidonAnimationRepeat(Animatoranimation){
}
});
animSet.start();
}
7、重置预备控件
??动画执行完毕之后,要做两个操作。第一个就是将当前不在显示范围的4个子控件左右各放2个,第二步就是为预备控件绑定新的星期日期。下面的方法是根据各个子控件的坐标来判断是隐藏还是展示,然后为隐藏的控件重新安排位置;然后绑定数据:
/这些引用代表当前正在显示的5个条目和四个预备条目,
由于ll_x系列条目是不断移动的,所以此处需要根据ll_x的位置重新为llx赋值
其中ll1-ll5为当前正在显示的条目,ll6、ll7为右边隐藏的预备条目,ll8、ll9为左边的隐藏预备条目
/
privateLinearLayoutll1,ll2,ll3,ll4,ll5,ll6,ll7,ll8,ll9;
/1、找到正在展示的五个item,并将预备item复位/
privatevoidsetCenter(WEEKDAYweekDay){
//记录当前显示在中间的星期X
centerNow=weekDay;
//1、找到当前显示的5个条目的位置
Listlist=newArrayList<>(llList);
for(inti=0;i<5;i++){
for(intj=0;j LinearLayoutll=list.get(j);
if(ll.getX()==ITEM_WIDTHi){
list.remove(ll);//找到之后就remove可以减少后面遍历的次数
//Log.d(TAG,"找到"+i+"了"+ll);
switch(i){
case0:
ll1=ll;
break;
case1:
ll2=ll;
break;
case2:
ll3=ll;
//当前中间位置item放大
ll3.getChildAt(0).setScaleX(scaleSize);
ll3.getChildAt(0).setScaleY(scaleSize);
break;
case3:
ll4=ll;
break;
case4:
ll5=ll;
break;
}
}
}
}
Log.i(TAG,"找完后还剩"+list.size()+"总:"+llList.size());
//2、剩余的四个作为预备,归位,左边隐藏两个,右边隐藏两个
for(inti=0;i LinearLayoutll=list.get(i);
switch(i){
case0://左1
ll.setX(-ITEM_WIDTH2);
ll8=ll;
break;
case1://左2
ll.setX(-ITEM_WIDTH1);
ll9=ll;
break;
case2://右1
ll.setX(ITEM_WIDTH5);
ll6=ll;
break;
case3://右2
ll.setX(ITEM_WIDTH6);
ll7=ll;
break;
}
}
//绑定数据
reBoundDataByCenter(weekDay);
}
/2、重新绑定数据/
privatevoidreBoundDataByCenter(WEEKDAYweekDay){
if(weekDay==WEEKDAY.wk1){
/星期1在中间,依次为4、5、6、7、1、2、3、4、5/
setLLText(ll8,4,false);
setLLText(ll9,5,false);
setLLText(ll1,6,false);
setLLText(ll2,7,false);
setLLText(ll3,1,true);
setLLText(ll4,2,false);
setLLText(ll5,3,false);
setLLText(ll6,4,false);
setLLText(ll7,5,false);
}elseif(weekDay==WEEKDAY.wk2){
/星期2在中间,依次为5、6、7、1、2、3、4、5、6/
setLLText(ll8,5,false);
setLLText(ll9,6,false);
setLLText(ll1,7,false);
setLLText(ll2,1,false);
setLLText(ll3,2,true);
setLLText(ll4,3,false);
setLLText(ll5,4,false);
setLLText(ll6,5,false);
setLLText(ll7,6,false);
}elseif(weekDay==WEEKDAY.wk3){
/星期3在中间,依次为6、7、1、2、3、4、5、6、7/
setLLText(ll8,6,false);
setLLText(ll9,7,false);
setLLText(ll1,1,false);
setLLText(ll2,2,false);
setLLText(ll3,3,true);
setLLText(ll4,4,false);
setLLText(ll5,5,false);
setLLText(ll6,6,false);
setLLText(ll7,7,false);
}elseif(weekDay==WEEKDAY.wk4){
/星期4在中间,依次为7、1、2、3、4、5、6、7、1/
setLLText(ll8,7,false);
setLLText(ll9,1,false);
setLLText(ll1,2,false);
setLLText(ll2,3,false);
setLLText(ll3,4,true);
setLLText(ll4,5,false);
setLLText(ll5,6,false);
setLLText(ll6,7,false);
setLLText(ll7,1,false);
}elseif(weekDay==WEEKDAY.wk5){
/星期5在中间,依次为1、2、3、4、5、6、7、1、2/
setLLText(ll8,1,false);
setLLText(ll9,2,false);
setLLText(ll1,3,false);
setLLText(ll2,4,false);
setLLText(ll3,5,true);
setLLText(ll4,6,false);
setLLText(ll5,7,false);
setLLText(ll6,1,false);
setLLText(ll7,2,false);
}elseif(weekDay==WEEKDAY.wk6){
/星期6在中间,依次为2、3、4、5、6、7、1、2、3/
setLLText(ll8,2,false);
setLLText(ll9,3,false);
setLLText(ll1,4,false);
setLLText(ll2,5,false);
setLLText(ll3,6,true);
setLLText(ll4,7,false);
setLLText(ll5,1,false);
setLLText(ll6,2,false);
setLLText(ll7,3,false);
}elseif(weekDay==WEEKDAY.wk7){
/星期7在中间,依次为3、4、5、6、7、1、2、3、4/
setLLText(ll8,3,false);
setLLText(ll9,4,false);
setLLText(ll1,5,false);
setLLText(ll2,6,false);
setLLText(ll3,7,true);
setLLText(ll4,1,false);
setLLText(ll5,2,false);
setLLText(ll6,3,false);
setLLText(ll7,4,false);
}
}
privatevoidsetLLText(LinearLayoutll,intwitchDay,booleanshowDate){
ll.setTag(witchDay);//便于区分点击事件
LinearLayoutinnerLL=(LinearLayout)ll.getChildAt(0);
TextViewtv=(TextView)innerLL.getChildAt(0);
Stringtext="星期"+WEEK_STR[witchDay];
tv.setText(text);
TextViewtvDate=(TextView)innerLL.getChildAt(1);
text=DATE_STR.get(witchDay);
tvDate.setText(text);
if(showDate){
tvDate.setVisibility(View.VISIBLE);
}else{
tvDate.setVisibility(View.GONE);
}
}
?当执行完上面的方法后,控件的状态又回到了初始状态,下次点击又会重复上面的步骤。因为动画执行完后,只有被隐藏的子控件需要重新安排位置,所以,不会出现闪跳的现象。
最终效果图如下:
|
|