自定义仿QQ主界面选项卡

时间:2022-02-04 06:28:12

自定义QQ主界面选项卡

写博客、效果、demo都是很花时间的,转载请注明出处!
http://blog.csdn.net/u011692041/article/details/60780603

QQ Android版本的效果先贴上来

自定义仿QQ主界面选项卡

可以看到这个可爱的选项卡,其实使用xml布局可以很容易的弄出来,但是博主就带大家封装成一个自定义控件!

博主实现的效果

自定义仿QQ主界面选项卡

这速度。。。抱歉哈,博主也不知道为啥这么快。。。。

可以看到,支持的还是挺丰富的,还支持包裹,根据自定义属性tabWidht来计算宽度
其实实现起来很简单,下面博主就带小白们来实现一下,大牛请忽略

分析


问题

实现上述的效果,如果我们是继承View,那么里面的文字、内边框、圆角效果都要自己绘制出来
而且还要支持字体大小的改变和里面文字的排列,显然这样子的代价太大
在我们平常的xml布局中,如果遇到类似的效果,我们很容易就可以想到线性布局
然后里面放几个文本控件并且使用权重进行等分宽度,所以今天的实现的思路就是这样的,只不过平时我们在xml手写的代码都封装起来!

总体思路

1.自定义控件继承LinearLayout,设置本身为水平
2.读取自定义属性到类中
3.根据所有自定义的属性往LinearLayout中添加TextView控件
4.最后就是被系统给绘制显示出来了(这步没我们的事..系统做)

额外的

1.圆角我们使用背景来实现就可以了,GradientDrawable完全可以胜任
2.添加TextView的点击事件,改变选中的下标,然后调用上述的第三步!
3.提供接口给使用者监听被选中的下标和文本
完工

首先国际惯例,决定继承的父类

自定义仿QQ主界面选项卡

三个参数的构造函数中就是我们上面说的几个步骤啦

支持的属性及其默认的值

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="XTabHost">
<!--半径-->
<attr name="radius" format="dimension" />
<!--文字大小-->
<attr name="text_size" format="dimension" />
<!--文本选中的颜色-->
<attr name="text_select_color" format="color" />
<!--文本未选中的颜色-->
<attr name="text_unselect_color" format="color" />
<!--tab选中的颜色-->
<attr name="tab_select_color" format="color" />
<!--tab没选中的颜色-->
<attr name="tab_unselect_color" format="color" />
<!--tab间距-->
<attr name="tab_space" format="dimension" />
<!--tab的宽,在包裹的时候用到-->
<attr name="tab_width" format="dimension"/>
<!--tab的高,在包裹的时候用到-->
<attr name="tab_height" format="dimension"/>
<!--整体的背景-->
<attr name="bg" format="color" />
<!--默认显示第几个-->
<attr name="default_index" format="integer" />
<!--显示的文本数组-->
<attr name="src" format="reference" />
</declare-styleable>
</resources>

对应到类中

    /**
* 自身控件的背景
*/

private int backBg = Color.WHITE;

/**
* 没有选中的tab的背景
*/

private int unSelectTabBg = Color.BLUE;

/**
* 选中的tab的背景
*/

private int selectTabBg = Color.WHITE;

/**
* 一个Tab的宽和高,在自身是包裹的时候会被用到
* -1表示不起作用,计算的时候按照包裹孩子处理
* 80是dp的单位
*/

private int tabWidth = 80, tabHeight = -1;

/**
* 未选中的文本的颜色
*/

private int unSelectTextColor = Color.WHITE;

/**
* 选中的文本的颜色
*/

private int selectTextColor = Color.BLUE;

/**
* 默认的字体大小,sp
*/

private int textSize = 16;

/**
* 间距,px
*/

private int space = 1;

/**
* 圆角半径,dp
*/

private int radius = 0;

/**
* 当前的下标
*/

private int curIndex = 1;

/**
* 所有要显示的文本
*/

private String[] textArr = new String[]{};

可以看到这些属性的效果在效果图中博主基本都使用出来了

读取自定义属性

    /**
* 读取自定义属性
*
* @param context
* @param attrs
*/

private void readAttr(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.XTabHost);

//获取自定义属性

curIndex = (int) a.getInt(R.styleable.XTabHost_default_index, 0);
radius = a.getDimensionPixelSize(R.styleable.XTabHost_radius, dpToPx(radius));
backBg = a.getColor(R.styleable.XTabHost_bg, Color.WHITE);
unSelectTabBg = a.getColor(R.styleable.XTabHost_tab_unselect_color,
Color.parseColor("#51B5EF"));
selectTabBg = a.getColor(R.styleable.XTabHost_tab_select_color, Color.WHITE);
Color.WHITE);
Color.parseColor("#51B5EF"));
textSize = a.getDimensionPixelSize(R.styleable.XTabHost_text_size, 16);
space = a.getDimensionPixelSize(R.styleable.XTabHost_tab_space, 1);
tabWidth = a.getDimensionPixelSize(R.styleable.XTabHost_tab_width,
dpToPx(tabWidth));
tabHeight = a.getDimensionPixelSize(R.styleable.XTabHost_tab_height, -1);

CharSequence[] arr = a.getTextArray(R.styleable.XTabHost_src);
if (arr != null) {
String[] tArr = new String[arr.length];
for (int i = 0; i < arr.length; i++) {
tArr[i] = String.valueOf(arr[i]);
}
textArr = tArr;
}

a.recycle();
}

由于每一个读取的属性在上面的定义的时候都有注释,就不做解释了

根据支持的属性生成效果

    /**
* 根据所有的参数,弄出效果
*/

private void sove() {

GradientDrawable dd = new GradientDrawable();
//设置圆角
dd.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
//设置背景颜色
dd.setColor(backBg);
//兼容低版本
if (Build.VERSION.SDK_INT >= 16) {
setBackground(dd);
} else {
setBackgroundDrawable(dd);
}

//移除所有的孩子
removeAllViews();

if (curIndex >= textArr.length || curIndex < 0) {
curIndex = 0;
}

for (int i = 0; i < textArr.length; i++) {

//创建一个文本
TextView tv = new TextView(getContext());

//创建文本的布局对象
LayoutParams params = new LayoutParams(
0, ViewGroup.LayoutParams.MATCH_PARENT
);

if (i > 0) {
params.leftMargin = space;
}

GradientDrawable d = getFitGradientDrawable(i);

//如果选中了设置选中的颜色和背景
if (curIndex == i) {
tv.setTextColor(selectTextColor);
d.setColor(selectTabBg);
} else {
tv.setTextColor(unSelectTextColor);
d.setColor(unSelectTabBg);
}

//设置文本
tv.setText(textArr[i]);
//设置文本显示在中间
tv.setGravity(Gravity.CENTER);
//设置文本大小
tv.setTextSize(textSize);
//设置文本的背景,兼容低版本
if (Build.VERSION.SDK_INT >= 16) {
tv.setBackground(d);
} else {
//noinspection deprecation
tv.setBackgroundDrawable(d);
}

//设置文本(也就是tab)的权重
params.weight = 1;

tv.setLayoutParams(params);

tv.setTag(i);
tv.setOnClickListener(this);

//添加孩子
addView(tv);

}

}
    /**
* 获取每一个tab的背景图,也就是TextView的背景图,最左边是左边有圆角效果的
* 最右边是右边有圆角效果的
* 即是左边又是右边的是四个角都有圆角的
*
* @param index tab的下标
* @return
*/

private GradientDrawable getFitGradientDrawable(int index) {
GradientDrawable d = null;
//根据下标决定圆角
if (index == 0 && index == textArr.length - 1) {//如果只有一个的时候
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
} else if (index == 0) { //如果是最左边的,那左上角和左下角是圆角
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{radius, radius, 0, 0, 0, 0, radius, radius});
} else if (index == textArr.length - 1) {//如果是最右边的,那右上角和右下角是圆角
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{0, 0, radius, radius, radius, radius, 0, 0});
} else { //如果是中间的,那么没有圆角
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{0, 0, 0, 0, 0, 0, 0, 0});
}
return d;
}

这段代码中点说一下

第一段长的方法sove是我们上面分析实现流程中的第三步,根据所有的属性实现效果
其实很简单,首先移除所有的孩子,然后根据文字的数组的个数,添加N个TextView,让每
一个TextView都是宽度平分父容器,高度填充父容器,这和自己在xml中写的效果是一样的
添加的过程中我们需要判断当前的Tab是否是带有圆角的,因为我们可以看到效果图中
左边的有圆角,右边的也有,所以方法getFitGradientDrawable(int index);
就是用来获取指定下标的背景,其实就是获取每一个TextView应该使用的背景
在for循环开始前我们可以看到我们也设置了本身的背景,本身的背景是四个角都有的哦
然后代码的最后再添加每一个TextView的点击事件,然后切换下选中的TextView和取消选中
的TextView的效果即可

剩下的代码

@Override
public void onClick(View v) {

//拿到下标
int index = (int) v.getTag();

//如果点击的是同一个,不做处理
if (index == curIndex) {
return;
}

//拿到当前的TextView
TextView tv = (TextView) getChildAt(curIndex);
//设置为没有被选中的文本和没有被选中的背景
tv.setTextColor(unSelectTextColor);
GradientDrawable d = getFitGradientDrawable(curIndex);
d.setColor(unSelectTabBg);

if (Build.VERSION.SDK_INT >= 16) {
tv.setBackground(d);
} else {
//noinspection deprecation
tv.setBackgroundDrawable(d);
}

//记录被选中的下标
curIndex = index;

//拿到当前被选中的TextView
tv = (TextView) getChildAt(curIndex);
//设置为被选中的文本和被选中的背景
tv.setTextColor(selectTextColor);
d = getFitGradientDrawable(curIndex);
d.setColor(selectTabBg);
if (Build.VERSION.SDK_INT >= 16) {
tv.setBackground(d);
} else {
//noinspection deprecation
tv.setBackgroundDrawable(d);
}

//如果使用者监听了就通知一下
if (mOnSelectListener != null) {
mOnSelectListener.onSelect(index, textArr[index]);
}

}

/**
* dp的单位转换为px的
*
* @param dps
* @return
*/

int dpToPx(int dps) {
return Math.round(getResources().getDisplayMetrics().density * dps);
}

/**
* sp转px
*
* @param spVal
* @return
*/

int spToPx(float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, getResources().getDisplayMetrics());
}

private OnSelectListener mOnSelectListener;

/**
* 设置监听
*
* @param mOnSelectListener
*/

public void setOnSelectListener(OnSelectListener mOnSelectListener) {
this.mOnSelectListener = mOnSelectListener;
}

/**
* 回调接口
*/

public interface OnSelectListener {

/**
* 回调方法
*
* @param index
* @param text
*/

void onSelect(int index, String text);

}

下面贴出所有的代码

/**
* Created by cxj on 2017/2/19.
* 模仿qq主界面的选项卡
*/

public class XTabHost extends LinearLayout implements View.OnClickListener {


public XTabHost(Context context) {
this(context, null);
}

public XTabHost(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public XTabHost(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//设置孩子排列的方向是水平的
setOrientation(HORIZONTAL);

//读取自定义属性
readAttr(context, attrs);

//显示效果
sove();

}

/**
* 读取自定义属性
*
* @param context
* @param attrs
*/

private void readAttr(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.XTabHost);

//获取自定义属性

curIndex = (int) a.getInt(R.styleable.XTabHost_default_index, 0);
radius = a.getDimensionPixelSize(R.styleable.XTabHost_radius, dpToPx(radius));
backBg = a.getColor(R.styleable.XTabHost_bg, Color.WHITE);
unSelectTabBg = a.getColor(R.styleable.XTabHost_tab_unselect_color, Color.parseColor("#51B5EF"));
selectTabBg = a.getColor(R.styleable.XTabHost_tab_select_color, Color.WHITE);
unSelectTextColor = a.getColor(R.styleable.XTabHost_text_unselect_color, Color.WHITE);
selectTextColor = a.getColor(R.styleable.XTabHost_text_select_color, Color.parseColor("#51B5EF"));
textSize = a.getDimensionPixelSize(R.styleable.XTabHost_text_size, 16);
space = a.getDimensionPixelSize(R.styleable.XTabHost_tab_space, 1);
tabWidth = a.getDimensionPixelSize(R.styleable.XTabHost_tab_width, dpToPx(tabWidth));
tabHeight = a.getDimensionPixelSize(R.styleable.XTabHost_tab_height, -1);

CharSequence[] arr = a.getTextArray(R.styleable.XTabHost_src);
if (arr != null) {
String[] tArr = new String[arr.length];
for (int i = 0; i < arr.length; i++) {
tArr[i] = String.valueOf(arr[i]);
}
textArr = tArr;
}

a.recycle();
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


//获取计算模式
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

//获取推荐的宽和高
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

if (modeWidth == MeasureSpec.EXACTLY) { //如果是确定的

} else { //如果是包裹的或者在横向的列表中
if (tabWidth > -1) {
for (int i = 0; i < getChildCount(); i++) {
TextView view = (TextView) getChildAt(i);
LayoutParams lp = (LayoutParams) view.getLayoutParams();
lp.width = tabWidth;
}
}
}

if (modeHeight == MeasureSpec.EXACTLY) { //如果是确定的

} else { //如果是包裹的或者在纵向的列表中
if (tabHeight > -1) {
for (int i = 0; i < getChildCount(); i++) {
TextView view = (TextView) getChildAt(i);
LayoutParams lp = (LayoutParams) view.getLayoutParams();
lp.height = tabHeight;
}
}

}

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

/**
* 自身控件的背景
*/

private int backBg = Color.WHITE;

/**
* 没有选中的tab的背景
*/

private int unSelectTabBg = Color.BLUE;

/**
* 选中的tab的背景
*/

private int selectTabBg = Color.WHITE;

/**
* 一个Tab的宽和高,在自身是包裹的时候会被用到
* -1表示不起作用,计算的时候按照包裹孩子处理
* 80是dp的单位
*/

private int tabWidth = 80, tabHeight = -1;

/**
* 未选中的文本的颜色
*/

private int unSelectTextColor = Color.WHITE;

/**
* 选中的文本的颜色
*/

private int selectTextColor = Color.BLUE;

/**
* 默认的字体大小,sp
*/

private int textSize = 16;

/**
* 间距,px
*/

private int space = 1;

/**
* 圆角半径,dp
*/

private int radius = 0;

/**
* 当前的下标
*/

private int curIndex = 1;

/**
* 所有要显示的文本
*/

private String[] textArr = new String[]{};


/**
* 根据所有的参数,弄出效果
*/

private void sove() {

GradientDrawable dd = new GradientDrawable();
//设置圆角
dd.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
//设置背景颜色
dd.setColor(backBg);
//兼容低版本
if (Build.VERSION.SDK_INT >= 16) {
setBackground(dd);
} else {
setBackgroundDrawable(dd);
}

//移除所有的孩子
removeAllViews();

if (curIndex >= textArr.length || curIndex < 0) {
curIndex = 0;
}

for (int i = 0; i < textArr.length; i++) {

//创建一个文本
TextView tv = new TextView(getContext());

//创建文本的布局对象
LayoutParams params = new LayoutParams(
0, ViewGroup.LayoutParams.MATCH_PARENT
);

if (i > 0) {
params.leftMargin = space;
}

GradientDrawable d = getFitGradientDrawable(i);

//如果选中了设置选中的颜色和背景
if (curIndex == i) {
tv.setTextColor(selectTextColor);
d.setColor(selectTabBg);
} else {
tv.setTextColor(unSelectTextColor);
d.setColor(unSelectTabBg);
}

//设置文本
tv.setText(textArr[i]);
//设置文本显示在中间
tv.setGravity(Gravity.CENTER);
//设置文本大小
tv.setTextSize(textSize);
//设置文本的背景,兼容低版本
if (Build.VERSION.SDK_INT >= 16) {
tv.setBackground(d);
} else {
//noinspection deprecation
tv.setBackgroundDrawable(d);
}

//设置文本(也就是tab)的权重
params.weight = 1;

tv.setLayoutParams(params);

tv.setTag(i);
tv.setOnClickListener(this);

//添加孩子
addView(tv);

}

}

/**
* 获取每一个tab的背景图,最左边是左边有圆角效果的
* 最右边是右边有圆角效果的
* 即是左边又是右边的是四个角都有圆角的
*
* @param index tab的下标
* @return
*/

private GradientDrawable getFitGradientDrawable(int index) {
GradientDrawable d = null;
//根据下标决定圆角
if (index == 0 && index == textArr.length - 1) {
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
} else if (index == 0) {
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{radius, radius, 0, 0, 0, 0, radius, radius});
} else if (index == textArr.length - 1) {
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{0, 0, radius, radius, radius, radius, 0, 0});
} else {
d = new GradientDrawable();
//设置圆角
d.setCornerRadii(new float[]{0, 0, 0, 0, 0, 0, 0, 0});
}
return d;
}


@Override
public void onClick(View v) {

//拿到下标
int index = (int) v.getTag();

//如果点击的是同一个,不做处理
if (index == curIndex) {
return;
}

//拿到当前的TextView
TextView tv = (TextView) getChildAt(curIndex);
//设置为没有被选中的文本和没有被选中的背景
tv.setTextColor(unSelectTextColor);
GradientDrawable d = getFitGradientDrawable(curIndex);
d.setColor(unSelectTabBg);

if (Build.VERSION.SDK_INT >= 16) {
tv.setBackground(d);
} else {
//noinspection deprecation
tv.setBackgroundDrawable(d);
}

//记录被选中的下标
curIndex = index;

//拿到当前被选中的TextView
tv = (TextView) getChildAt(curIndex);
//设置为被选中的文本和被选中的背景
tv.setTextColor(selectTextColor);
d = getFitGradientDrawable(curIndex);
d.setColor(selectTabBg);
if (Build.VERSION.SDK_INT >= 16) {
tv.setBackground(d);
} else {
//noinspection deprecation
tv.setBackgroundDrawable(d);
}

//如果使用者监听了就通知一下
if (mOnSelectListener != null) {
mOnSelectListener.onSelect(index, textArr[index]);
}

}

/**
* dp的单位转换为px的
*
* @param dps
* @return
*/

int dpToPx(int dps) {
return Math.round(getResources().getDisplayMetrics().density * dps);
}

/**
* sp转px
*
* @param spVal
* @return
*/

int spToPx(float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, getResources().getDisplayMetrics());
}

private OnSelectListener mOnSelectListener;

/**
* 设置监听
*
* @param mOnSelectListener
*/

public void setOnSelectListener(OnSelectListener mOnSelectListener) {
this.mOnSelectListener = mOnSelectListener;
}

/**
* 回调接口
*/

public interface OnSelectListener {

/**
* 回调方法
*
* @param index
* @param text
*/

void onSelect(int index, String text);

}

}

喜欢博主的朋友可以关注一波哦。。。
有问题及时在评论去留言,博主会第一时间解答的
最近博主也是比较忙,文章中必有不详细之处,但是最大的好处
就是博主的注释还是很详细的,弥补一下吧。。

下载源码

源码下载