微信主页面慢慢水平滑动时,会发现底部栏的图标以及文字有透明度渐变效果。其中,透明度的大小依赖ViewPager的Offset系数。从当前页面切换到下一个页面,当前页面的底部栏颜色会逐渐变淡,而下一个页面的底部栏颜色会逐渐变深。其实底部栏渐变效果已经有其他人写过博客以及发表过开源项目,但是觉得还是缺少点什么。打开微信看看,噢耶,是红点提醒和未读统计。既然有完善空间,那么说做就做。效果图如下:
其实,抛开透明度渐变效果,RadioGroup和RadioButton就可以实现页面切换。顺着这个思路,自定义控件分别继承RadioGroup和RadioButton,根据ViewPager切换页面的偏移值Offset系数,改变RadioButton的Alpha透明度数值,这样就可实现渐变效果。
//根据偏移值更新透明度
private void updateOffsetAlpha(int position, float positionOffset){
((AlphaRadioButton)getChildAt(position)).updateAlpha(255 * (1 - positionOffset));
((AlphaRadioButton)getChildAt(position + 1)).updateAlpha(255 * positionOffset);
}
//ViewPager监听positionOffset
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if(positionOffset > 0)//offset发生变化,根据offset改变child的alpha
updateOffsetAlpha(position, positionOffset);
}
这里遍历所有RadioButton,调用对应的RadioButton去改变透明度,当前页面Alpha=255 * (1 - positionOffset),将要切换到的页面Alpha=255 * positionOffset。每次改变透明度都触发onDraw方法,在里面重新绘制图标与文字。
//根据alpha值改变当前页面的底部栏文字透明度
private void drawUnSelectedText(Canvas canvas) {
mTextPaint.setColor(getCurrentTextColor());
mTextPaint.setAlpha(255 - mAlpha);
canvas.drawText(getText().toString(), getWidth() / 2 - mTextWidth / 2
, mIconHeight + getPaddingTop() + mFontHeight, mTextPaint);
}
//根据alpha值改变下一个页面的底部栏文字透明度
private void drawSelectedText(Canvas canvas) {
mTextPaint.setColor(mColor);
mTextPaint.setAlpha(mAlpha);
canvas.drawText(getText().toString(), getWidth() / 2 - mTextWidth / 2
, mIconHeight + getPaddingTop() + mFontHeight, mTextPaint);
}
在xml布局文件,引入自定义控件,并设置选中图标、未选中图标、选中的颜色、图标宽度与高度。如下所以:
<com.frank.bottombar.AlphaRadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_bar_size"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<com.frank.bottombar.AlphaRadioButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:checked="true"
android:gravity="center"
android:paddingTop="3dp"
android:text="@string/message"
android:textColor="@color/light_gray"
android:textSize="13sp"
app:icon_unselected="@drawable/message"
app:color_selected="@color/green"
app:icon_selected="@drawable/message_green"
app:icon_height_default="25dp"
app:icon_width_default="25dp" />
<com.frank.bottombar.AlphaRadioButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:paddingTop="3dp"
android:text="@string/contact"
android:textColor="@color/light_gray"
android:textSize="13sp"
app:icon_unselected="@drawable/contact"
app:color_selected="@color/green"
app:icon_selected="@drawable/contact_green"
app:icon_height_default="25dp"
app:icon_width_default="25dp" />
<com.frank.bottombar.AlphaRadioButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:paddingTop="3dp"
android:text="@string/discover"
android:textColor="@color/light_gray"
android:textSize="13sp"
app:icon_unselected="@drawable/discover"
app:color_selected="@color/green"
app:icon_selected="@drawable/discover_green"
app:icon_height_default="25dp"
app:icon_width_default="25dp" />
<com.frank.bottombar.AlphaRadioButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:paddingTop="3dp"
android:text="@string/about_me"
android:textColor="@color/light_gray"
android:textSize="13sp"
app:icon_unselected="@drawable/about_me"
app:color_selected="@color/green"
app:icon_selected="@drawable/about_me_green"
app:icon_height_default="25dp"
app:icon_width_default="25dp" />
</com.frank.bottombar.AlphaRadioGroup>
当然,这些自定义属性需要在attrs.xml文件中声明:
<declare-styleable name="BottomBar">
<attr name="icon_selected" format="reference"></attr>
<attr name="icon_unselected" format="reference"></attr>
<attr name="color_selected" format="color"></attr>
<attr name="icon_width_default" format="dimension"></attr>
<attr name="icon_height_default" format="dimension"></attr>
</declare-styleable>
接下来是红点提醒与未读数统计。它们只要调用一句代码,参入需要显示的position、count就可以。当然,不可能只有显示,永远显示那就成为无解bug了。同样地,调用一句代码就可以把显示的红点或未读统计隐藏。这里开子线程,只是为了测试是否可以更在子线程调用以下方法。
alphaRadioGroup.showDot(1);//显示红点
alphaRadioGroup.showUnreadCount(0, 9);//显示未读数量统计
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(2000);
alphaRadioGroup.hideUnreadCount(0);//隐藏未读数量统计
SystemClock.sleep(2000);
alphaRadioGroup.hideDot(1);//隐藏红点
}
}).start();
理论上,只能在主线程直接操作UI界面的,也就是调用invalidate方法。在这里考虑到子线程也可能会调用,所以调用方法前加了当前线程判断,如果是子线程,改为调用postInvalidate方法。
// 显示红点
public void showDot(int position){
checkPosition(position);
((AlphaRadioButton)getChildAt(position)).showDot(true);
}
//隐藏红点
public void hideDot(int position){
checkPosition(position);
((AlphaRadioButton)getChildAt(position)).showDot(false);
}
// 显示未读数量
public void showUnreadCount(int position, int count){
checkPosition(position);
((AlphaRadioButton)getChildAt(position)).showUnreadCount(true, count);
}
// 隐藏未读数量
public void hideUnreadCount(int position){
checkPosition(position);
((AlphaRadioButton)getChildAt(position)).showUnreadCount(false, 0);
}
// 检查绘制的position合法性
protected void checkPosition(int position){
if(position < 0 || position >= getChildCount())
throw new IndexOutOfBoundsException("The position to show dot should not be smaller than 0 " +
"or greater than childCount");
}
上面方法是调用RadioButton的更新页面,被调用方法如下:
//是否显示红点
public void showDot(boolean drawDot){
this.drawDot = drawDot;
if(isMainThread())
invalidate();
else
postInvalidate();
}
//是否显示未读数量
public void showUnreadCount(boolean drawCount, int count){
this.drawCount = drawCount;
this.count = count;
if(isMainThread())
invalidate();
else
postInvalidate();
}
在onDraw方法里,具体实现绘制页面,显示红点或者未读统计的执行代码如下:
//在图标右上角画红点
public void drawDot(Canvas canvas){
mDotPaint.setColor(Color.RED);
canvas.drawCircle((getWidth() + mIconWidth)/2, getPaddingTop() + 10, dp2px(4), mDotPaint);
}
//在图标右上角画未读数量
public void drawCount(Canvas canvas, int count){
mDotPaint.setColor(Color.RED);
mCountPaint.setColor(Color.WHITE);
if(count < 10){//1-9
mCountPaint.setTextSize(sp2px(12));
canvas.drawCircle((getWidth() + mIconWidth)/2, getPaddingTop() + dp2px(7), dp2px(9), mDotPaint);
canvas.drawText(String.valueOf(count), (getWidth() + mIconWidth)/2 - dp2px(3),
getPaddingTop() + dp2px(11), mCountPaint);
}else if(count < 100){//10-100
mCountPaint.setTextSize(sp2px(10));
canvas.drawCircle((getWidth() + mIconWidth)/2, getPaddingTop() + dp2px(7), dp2px(9), mDotPaint);
canvas.drawText(String.valueOf(count), (getWidth() + mIconWidth)/2 - dp2px(6),
getPaddingTop() + dp2px(10), mCountPaint);
}else {//大于100
mCountPaint.setTextSize(sp2px(8));
canvas.drawCircle((getWidth() + mIconWidth)/2, getPaddingTop() + dp2px(7), dp2px(9), mDotPaint);
canvas.drawText("99+", (getWidth() + mIconWidth)/2 - dp2px(6),
getPaddingTop() + dp2px(11), mCountPaint);
}
}
可以看到,绘制红点提醒相对简单。但是绘制未读数量时,考虑到圆圈背景大小不变,而不同位数的数字所占的位置不同,这里分三种情况:1-9,10-99和99以上。两位数的字体尺寸需要改小一点,99条以上统一显示"99+"。另外,考虑到不同分辨率手机适配问题,使用到dp2px和sp2px方法进行单位转换。
好了,透明度渐变效果加上红点提醒与未读统计介绍完了。如果大家有什么问题,欢迎交流,同样也欢迎分享其它效果。