课程咨询: 400-996-5531 / 投诉建议: 400-111-8989
认真做教育 专心促就业
微信相信大家都不陌生,作为一款社交软件,他慢慢的潜入了我们的生活之中,许多人的休闲时间很大一部分都分配给了微信,那么,大家有没有对微信的编程代码是如何实现某些效果有所关注呢?
天天使用微信,作为一个android developer,当然会对微信的底部tab感兴趣,刚好工作中要开发一个bbs客户端,gui也要求实现一个类似微信底部tab效果的控件, 开始在网上狂搜一通,也找到了类似的东西(至于是哪位大神的,我已经忘了连接,这里就不详述了),进过自己进一步的封装,自认为完美的实现了gui的设计需求, 然而现在在回想实现的细节,却很难想起其中的关键点,于是乎,决定自己重新写一个,加深自定义ViewGroup和自定义View。
该控件的技术难点:
如果通过监听ViewPager的滑动来实现Tab切换的渐变效果
当然对我来说自己重新实现就不止上述一个难点了,下面就让我们还是从源代码中逐步分析如何实现自定义的TabSelectorLayout
实现过程
上述技术难点如何解决
对于Tab,显示状态只有两个,一个select,一个normal,大家仔细观察微信的渐变效果,其实就是在滑动page的时候,选择的item就是select状态逐渐显示, 而normal状态逐渐消失,而失去select状态的item,与之相反;那么我们就可以通过控制两个状态的drawable的alpha值(状态之间属于”补集”)来实现状态切换 的渐变效果
定义View的属性
[attrs.xml]
定义Tab属性类
public static final class Tab { //TabSelectorLayout嵌套类
Drawable normalDrawable; //自然状态图片
Drawable selectDrawable; //选中状态的图片
String title; //文字
int position; //tab的位置
private Tab() { // TabSelectorLayout外部无法创建该类的对象
}
Tab setPosition(int position) { //TabSelectorLayout外部无法访问该方法
this.position = position;
return this;
}
public int getPosition() {
return position;
}
public Tab setTitle(String title) {
this.title = title;
return this;
}
public Tab setNormalDrawable(Drawable d) {
normalDrawable = d;
return this;
}
public Tab setSelectDrawable(Drawable d) {
selectDrawable = d;
return this;
}
}
TabSelectorLayout的属性
private ViewPager viewPager; //和关联的ViewPager
private int textSize = 12; //文字大小
private int drawablePadding = 10; //drawable和文字之间的距离
private int normalTextColor, selectTextColor; //文字两种状态的颜色值
构造方法(解析xml属性)
public TabSelectorLayout(Context context) {
super(context);
}
public TabSelectorLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TabSelectorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = null;
try {
array = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TabSelectorLayout, defStyleAttr, 0);
int count = array.getIndexCount();
for (int i = 0; i < count; i++) {
final int index = array.getIndex(i);
if (index == R.styleable.TabSelectorLayout_normalTextColor) {
normalTextColor = array.getColor(index, Color.BLACK);
} else if (index == R.styleable.TabSelectorLayout_selectTextColor) {
selectTextColor = array.getColor(index, Color.BLACK);
} else if (index == R.styleable.TabSelectorLayout_drawablePadding) {
drawablePadding = array.getDimensionPixelSize(index, 10);
} else if (index == R.styleable.TabSelectorLayout_android_textSize) {
textSize = array.getDimensionPixelSize(index,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, getResources().getDisplayMetrics()));
}
}
Log.d(TAG, "normalTextColor = " + normalTextColor + " drawablePadding = " + drawablePadding + " textSize = " + textSize);
} finally {
if (array != null) {
array.recycle();
}
}
}
提供获得Tab对象的方法
public static Tab newTab() {
return new Tab();
}
将初始化好的Tab添加到TabSelectorLayout中
public int addTab(final Tab tab) { //返回添加tab的posion
//参数判断
if (tab == null) {
throw new NullPointerException("tab is null");
}
if (tab.title == null || tab.selectDrawable == null || tab.normalDrawable == null) {
throw new IllegalArgumentException("some argument is null");
}
final TabView tabView = new TabView(getContext()); //new 出显示的TabView
tabView.attach(tab); //关联tab到TabView中
tabView.setOnClickListener(new OnClickListener() { //tab的click监听,切换ViewPager的显示
@Override
public void onClick(View v) {
viewPager.setCurrentItem(tab.getPosition(), false); //切换不滚动
}
});
addView(tabView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
tab.setPosition(indexOfChild(tabView)); //设置tab的position
return tab.position;
}
重写addView对添加的child做以下限制
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) { //只能添加TabView
if (child instanceof TabView) {
super.addView(child, index, params);
} else {
throw new IllegalArgumentException("child is not TabView");
}
}
关联ViewPager
public void bindViewPager(ViewPager pager) {
//参数处理
if (pager == null)
throw new NullPointerException("pager is null");
if (viewPager == pager)
return;
if (viewPager != null) //remove对上个ViewPager的监听
viewPager.removeOnPageChangeListener(pageChangeListener);
//ViewPager的Adapter数据判读
PagerAdapter adapter = pager.getAdapter();
if (adapter == null)
throw new IllegalArgumentException("pager not set adapter");
if (adapter.getCount() != getChildCount())
throw new IllegalArgumentException("pager count is not equeals tab count");
pager.addOnPageChangeListener(pageChangeListener); //添加OnPageChangeListener
viewPager = pager;
setCurrentItem(viewPager.getCurrentItem()); //设置显示的Tab
}
OnPageChangeListener的实现,给TabView传入ViewPager滑动变化数据
private ViewPager.OnPageChangeListener pageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
TabView cur = (TabView) getChildAt(position);
if (positionOffset > 0) {
cur.setTabOffSet(1 - positionOffset);
TabView next = (TabView) getChildAt(position + 1);
next.setTabOffSet(positionOffset);
} else {
cur.setTabOffSet(1 - positionOffset);
}
}
@Override
public void onPageSelected(int position) {
setCurrentItem(position);
}
};
切换Tab方法
public void setCurrentItem(int item) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
TabView child = (TabView) getChildAt(i);
child.setSelected(item == i); //修改TabView的选中状态
}
}
万年不变的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
final int count = getChildCount();
int childWidth = (width - getPaddingLeft() - getPaddingRight()) / count; //每个child的宽度一样
int maxChildHeight = 0; //所有child中最高的值
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
//测量child,精确child的宽度,设定child的最大高度
child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
int h = child.getMeasuredHeight();
int w = child.getMeasuredWidth();
maxChildHeight = maxChildHeight > h ? maxChildHeight : h;
}
}
int actionBarHeight = getActionBarHeight(); //默认Layout的高度是ActionBar的高度
int h = actionBarHeight > maxChildHeight ? actionBarHeight : maxChildHeight;
setMeasuredDimension(width, h + getPaddingTop() + getPaddingBottom());
}
获取actionBar的高度
private int getActionBarHeight() {
TypedValue tv = new TypedValue();
if (getContext().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true))
return TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
return 0;
}
必须实现的onLayout方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
int lefPos = getPaddingLeft(); //child的left位置
final int parentTop = getPaddingTop();
final int parentBottom = bottom - top - getPaddingBottom();
final int parentHeight = parentBottom - parentTop;
final Rect childRect = new Rect(); //child的显示矩阵
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
lefPos += lp.leftMargin; //变化下一个child的left
childRect.left = lefPos;
childRect.top = (parentHeight - height) / 2;
childRect.bottom = childRect.top + height;
childRect.right = childRect.left + width;
// Use the child's gravity and size to determine its final
// frame within its container.
//Gravity.apply(lp.gravity, width, height, mTmpContainerRect, mTmpChildRect);
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); //layout child
lefPos += width; //变化下一个child的left
lefPos += lp.rightMargin; //变化下一个child的left
}
}
定义TabView
private class TabView extends View { //私有内部类
TabView属性
private int normalAlpha = 255; //自然状态下的alpha
private int viewWidth; //TabView宽度
private int viewHeight;//TabView高度
private final Paint textNormalPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); //自然状态text画笔
private final Paint textSelectPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);//选中状态text画笔
private final Rect boundText = new Rect(); //text的bound
private Tab tab; //关联的tab
TabView属性初始化
private void initText() {
textNormalPaint.setColor(normalTextColor);
textNormalPaint.setTextSize(textSize);
textSelectPaint.setColor(selectTextColor);
textSelectPaint.setTextSize(textSize);
}
获得指定字体大小text的显示矩阵
private void measureText() {
textNormalPaint.getTextBounds(tab.title, 0, tab.title.length(), boundText);
}
通过监听到的ViewPager滑动变化来修改显示的alpha
void setTabOffSet(float offSet) {
normalAlpha = (int) (255 - offSet * 255);
invalidate();
}
重新setSelected方法,来修改显示的alpha
@Override
public void setSelected(boolean selected) {
normalAlpha = selected ? 0 : 255;
super.setSelected(selected);
}
TabView的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int w = 0, h = 0;
measureText();//获得指定字体大小text的显示矩阵
//显示内容的最大宽度
int contentWidth = Math.max(boundText.width(), Math.max(tab.normalDrawable.getIntrinsicWidth(), tab.selectDrawable.getIntrinsicWidth()));
//期望的宽度
int desiredWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
switch (widthMode) {
case MeasureSpec.AT_MOST:
w = Math.min(width, desiredWidth);
break;
case MeasureSpec.EXACTLY: //实现的ViewGroup中已经指定精确TabView的宽度
w = width;
break;
case MeasureSpec.UNSPECIFIED:
w = desiredWidth;
break;
}
//获得显示内容的高度,注意这里计算text的高度使用了Paint的getFontSpacing()方法,原因可以参考文后资料
int contentHeight = (int) (textNormalPaint.getFontSpacing() + Math.max(tab.normalDrawable.getIntrinsicHeight(), tab.selectDrawable.getIntrinsicHeight()) + drawablePadding);
//期望的最大高度
int desiredHeight = getPaddingTop() + getPaddingBottom() + contentHeight;
switch (heightMode) {
case MeasureSpec.AT_MOST:
h = Math.min(height, desiredHeight);
break;
case MeasureSpec.EXACTLY:
h = height;
break;
case MeasureSpec.UNSPECIFIED:
h = height;
break;
}
setMeasuredDimension(w, h);
viewWidth = getMeasuredWidth();
viewHeight = getMeasuredHeight();
}
TabView的onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawNoramalBitmap(canvas);
drawSelectBitmap(canvas);
drawText(canvas);
}
TabView的自然状态drawable的绘制
private void drawNoramalBitmap(Canvas canvas) {
int width = tab.normalDrawable.getIntrinsicWidth();
int height = tab.normalDrawable.getIntrinsicHeight();
int left = (viewWidth - width) / 2;
int top = (viewHeight - height - boundText.height() - drawablePadding) >> 1;
tab.normalDrawable.setBounds(left, top, left + width, top + height);
tab.normalDrawable.setAlpha(normalAlpha); //修改绘制drawable的alpha
tab.normalDrawable.draw(canvas);
}
TabView的选中状态drawable的绘制
private void drawSelectBitmap(Canvas canvas) {
int width = tab.selectDrawable.getIntrinsicWidth();
int height = tab.selectDrawable.getIntrinsicHeight();
int left = (viewWidth - width) / 2;
int top = (viewHeight - height - boundText.height() - drawablePadding) >> 1;
tab.selectDrawable.setBounds(left, top, left + width, top + height);
tab.selectDrawable.setAlpha(255 - normalAlpha); //修改绘制drawable的alpha
tab.selectDrawable.draw(canvas);
}
TabView的text的绘制
private void drawText(Canvas canvas) {
int drawableHeight = Math.max(tab.normalDrawable.getIntrinsicHeight(), tab.selectDrawable.getIntrinsicHeight());
float x = (viewWidth - boundText.width()) / 2.0f;
float y = (viewHeight + drawableHeight + boundText.height() + drawablePadding) >> 1;
textNormalPaint.setAlpha(normalAlpha); //修改绘制text自然状态的alpha
canvas.drawText(tab.title, x, y, textNormalPaint);
textSelectPaint.setAlpha(255 - normalAlpha); //修改绘制text选中状态的alpha
canvas.drawText(tab.title, x, y, textSelectPaint);
}
如何使用
使用的xml代码
android:id="@+id/tabselectorlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:selectTextColor="@android:color/holo_green_dark"
app:normalTextColor="@android:color/darker_gray"
app:drawablePadding="5dp"
android:textSize="13sp"/>
使用的java代码
//首先获得TabSelectorLayout对象
mTabSelectorLayout = (TabSelectorLayout) findViewById(R.id.tabselectorlayout);
...
//添加tab
mTabSelectorLayout.addTab(TabSelectorLayout.newTab()
.setNormalDrawable(resources.getDrawable(R.drawable.ic_tab_moment))
.setSelectDrawable(resources.getDrawable(R.drawable.ic_tab_moment_select))
.setTitle("moment"));
...
//和ViewPager绑定
mTabSelectorLayout.bindViewPager(mViewPager);
文章感想
写完本文就觉得自己啰嗦了,总想将所有的内容都写出来,哪怕再简单的东西,大量的代码对读者来说都很简单,但我只想展示自己实现这个控件一步步的过程, 期望自己以后的文章尽量提升blog的水平吧
达内时代科技集团致力于培养面向电信和金融领域的Java、C++、C#/.Net、3G/Android、3G/IOS、PHP、嵌入式、软件测试、UID、网络营销、网络工程、会计、UED、web、Unity3D、大数据、童程童美等17大方向中高端软件人才课程与少儿教育课程。选择达内运城it培训,不再孤军奋战,轻轻松松做IT高薪白领。运城达内培训带领有明确目标的学子迈向成功之路!想找工作的求职者可以加QQ:3373924515(太原达内就业服务部)咨询了解。