Android 自定义控件之ViewPageIndicator
久不做android,感觉自己的技术都生疏了,虽然说,感觉自己也没什么技术:)
另:本篇博客参考的是鸿洋大神的一篇博客,再次附上链接地址http://www.cnblogs.com/freeliver54/archive/2012/08/21/2649496.html,拜谢!
前言:虽然说网上关于ViewPageIndicator的轮子是一个又一个,但是,会用是一回事,会写,又是另一回事,如果事事都想着搬轮子,那和咸鱼有什么区别。
好了,闲话不多说,下面就来进入正题吧。
做一个东西,首先重要的是思路,思路清晰了,写起来才会顺手,而不至于茫然四顾,举足无措。下面来分析一下这个自定义控件要怎么写?
android自定义控件的实现有两种方式,一是控件的组合,二就是通过画笔来进行绘制了,显然viewPageIndicator的话,直接就用控件的组合来做就行了,而且参考到常见的一些自定义ViewPageIndicator,继承LinearLayout显然是不错的选择,Tab项,不讲求华丽的话,TextView就行了,然后考虑到tab项可能一个屏幕显示不下,因此需要规定一个页面显示几个tab,哦,也就是TextView,这个可以通过提供方法进行设置,也可以提供自定义属性。
最后,最重要的就是就是指示器了,这里和鸿洋大神一样,也是采用三角形了,至于偏移量,大体就是一个(textview的宽度-三角形的宽度)/2+textview的宽度*n。
大体思路就是这样了,下面看一下用法:
1、通用setItemTitle动态设置tab
<study.hugy.com.myapp.customView.ViewPagerIndicator.ViewPagerIndicator
android:id="@+id/mIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/top_bar"
android:minHeight="40dp"
android:orientation="horizontal"
app:visible_count="3"/>
<android.support.v4.view.ViewPager
android:id="@+id/home_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/mIndicator">
</android.support.v4.view.ViewPager>
public class HomePageFragement extends Fragment {
private ViewPagerIndicator indicator;
private ViewPager viewPager;
private static List<String> title = new ArrayList<>();
static {
title.add("目录一");
title.add("目录二");
title.add("目录三");
title.add("目录四");
title.add("目录五");
}
private PagerAdapter mAdapter = new PagerAdapter() {
@Override
public int getCount()
{
return title.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
TextView view = new TextView(context);
view.setText(position+"");
view.setTextColor(Color.WHITE);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
};
private Context context;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
context = getActivity().getApplicationContext();
View view=inflater.inflate(R.layout.fragment_home_page,container,false);
indicator = (ViewPagerIndicator) view.findViewById(R.id.mIndicator);
viewPager = (ViewPager) view.findViewById(R.id.home_view_pager);
indicator.setItemTitles(title);
viewPager.setAdapter(mAdapter);
indicator.setViewPager(viewPager,0);
Log.i("hugy","childCounts:"+indicator.getChildCount()); return view;
}
}
可以看到,真正涉及到ViewPageIndicator用法的代码只有两行
indicator.setItemTitles(title);
indicator.setViewPager(viewPager,0);
好了,基本的用法就是这样了,下面来看一看代码的实现。
首先是自定义属性,在res/values目录下新建attrs.xml文件,并做如下声明
<attr name="visible_count" format="integer"></attr>
<declare-styleable name="ViewPagerIndicator">
<attr name="visible_count"/>
</declare-styleable>
然后看一下需要哪些属性:
private final int DEFAULT_VISIBLE_COUNT = 4; //默认一个屏幕显示的tab数量,在未设置自定义属性的时候使用
private final float REDIO_TRIENGLE = (float) (1.0 / 6); //一个三角形指示器占用一个条目的1/6宽度
private final int COLOR_TEXT_NORMAL = 0x77FFFFFF; //textView未被选择时字体的颜色
private final int COLOR_TEXT_SELECTED = 0xFFFFFFFF; //textView被选择时字体的颜色
private int DEFAULT_TRIENGLE_WIDTH = 0; //默认的三角形的宽度
private int mVisibleItemCount = DEFAULT_VISIBLE_COUNT; //一个屏幕的tab的数量,默认等于DEFAULT_VISIBLE_COUNT
private Context mContext;//上下文
private Paint mPaint; //绘制指示器使用
private int mTriengleWidth; //三角形的宽度
private Path mPath; //绘制三角形的路径
private int mTriengleHeight; //三角形的高度
private int mInitTranslationX; //指示器的初始偏移量
private float mTranslationX; //指示器的偏移量
private ViewPager mViewPager; //关联的viewPager
private List<String> itemList; //tab显示的内容
然后是构造函数
public ViewPagerIndicator(Context context) { this(context,null);}
public ViewPagerIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
DEFAULT_TRIENGLE_WIDTH = (int) (getScreenWidth()/3*REDIO_TRIENGLE);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerIndicator);
mVisibleItemCount = a.getInt(R.styleable.ViewPagerIndicator_visible_count,
DEFAULT_VISIBLE_COUNT);
if (mVisibleItemCount < 0 ){
mVisibleItemCount = DEFAULT_VISIBLE_COUNT;
}
a.recycle();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.parseColor("#ffffffff"));
mPaint.setStyle(Paint.Style.FILL);
mPaint.setPathEffect(new CornerPathEffect(3));
}
可以看到,在构造中我们主要做了2件事,一是获得自定义的visible_count属性,二是初始化mPaint画笔;
然后是onFinishInflate方法和onSizeChanged方法,其中onFInishInflate方法是在控件实例化后调用,此方法主要是处理使用者 ViewPageIndicator中定义了Tab的情况,不理解的话可以看一下方法中的代码,而onSizeChanged和显然就是当控件的尺寸变化时调用,这个会 在控件实例化的时候调用;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int cCount = getChildCount();
if (cCount == 0) return;
for (int i = 0; i < cCount; i++) {
View view = getChildAt(i);
LinearLayout.LayoutParams params = (LayoutParams) view.getLayoutParams();
params.width = 0;
params.width = getScreenWidth()/mVisibleItemCount;
view.setLayoutParams(params);
}
setClickEvent();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mTriengleWidth = (int) ((w/mVisibleItemCount)*REDIO_TRIENGLE);
mTriengleWidth = Math.min(DEFAULT_TRIENGLE_WIDTH,mTriengleWidth);
initTriengle();
mInitTranslationX = (getWidth() / mVisibleItemCount - mTriengleWidth) / 2;
}
//移动画布到初始位置
@override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.translate(mInitTranslationX + mTranslationX, getHeight() + 1);
canvas.drawPath(mPath, mPaint);
canvas.restore();
super.dispatchDraw(canvas);
}
onFinishInflate方法的作用上面已经说过了,相信大家看一下就会明白,下面说一下onSizeChanged方法,其实也没什么说的,相信大家也一眼就看的出来,其实这个方法是就是初始化指示器用的,想要对指示器形状做文章的朋友
可以在initTriengle方法中修改,下面贴上代码
private void initTriengle() {
mPath = new Path();
mTriengleHeight = (int) (mTriengleWidth/2/Math.sqrt(2));
mPath.moveTo(0,0);
mPath.lineTo(mTriengleWidth,0);
mPath.lineTo(mTriengleWidth/2,-mTriengleHeight);
mPath.close();
}
private void setClickEvent() {
for (int i = 0; i < getChildCount(); i++) {
final int finalI = i;
getChildAt(i).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mViewPager.setCurrentItem(finalI);
invalidate();
}
});
}
}
这样,内部的方法基本上就写完了,下面来看看对外暴露的方法
首先是setItemTitle(List<String> data)
public void setItemTitles(List<String> data){
this.removeAllViews();
for (String str:data){
TextView view = new TextView(mContext);
view.setText(str);
view.setTextColor(COLOR_TEXT_NORMAL);
// view.setWidth(getScreenWidth()/mVisibleItemCount);
view.setTextAlignment(TEXT_ALIGNMENT_CENTER);
this.addView(view);
}
Log.i("hugy","childCounts:"+getChildCount());
setClickEvent();
onFinishInflate();
}
关于这个方法,要说的就是末尾的inFinishInflate方法了,这个方法的作用在于给包裹TextView的
ViewPagerIndicator重新分配大小,举个例子吧,如果你有5个tab项,你的每一个tab的宽度为1/3的
ViewPagerIndicator的宽度,那么,如果不手动执行onFinishInflate方法的话,你能看到的条目就只有
三条,后面两条是看不见的,因为你的ViewPageIndicator的宽度固定为了屏幕的宽度,这个宽度内
只能容纳三个,多的就显示不了了,而调用了onFinishInflate之后,重新给ViewPageIndicator分
配了宽度,因此剩余的条目也就可见了,因此手动调用此方法是不可缺少的;
然后就是对外暴露的第二个方法setViewPager(ViewPager viewPager,int pos)了
public void setViewPager(ViewPager viewPager,int pos){
this.mViewPager = viewPager;
this.mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
scroll(position,positionOffset);
if (pageChangerListener != null){
pageChangerListener.onPageScrolled(position,positionOffset,positionOffsetPixels);
}
}
@Override
public void onPageSelected(int position) {
resetTextViewColor();
highLightTextView(position);
scroll(position,0);
if (pageChangerListener != null){
pageChangerListener.onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (pageChangerListener != null){
pageChangerListener.onPageScrollStateChanged(state);
}
}
});
mViewPager.setCurrentItem(pos);
highLightTextView(pos);
}
highLightTextView(pos)和resetTextView()就不多说了,都是改变textView中的字体的颜色,关键的是
scroll(pos,offset)方法
下面是代码
private void scroll(int position, float positionOffset) {
mTranslationX = getWidth() / mVisibleItemCount * (position + positionOffset);
int tabWidth = getScreenWidth() / mVisibleItemCount;
if (positionOffset > 0 && position >= (mVisibleItemCount - 2)
&& getChildCount() > mVisibleItemCount)
{
if (mVisibleItemCount != 1)
{
this.scrollTo((position - (mVisibleItemCount - 2)) * tabWidth
+ (int) (tabWidth * positionOffset), 0);
} else
// 为count为1时 的特殊处理
{
this.scrollTo(
position * tabWidth + (int) (tabWidth * positionOffset), 0);
}
}
invalidate();
}
主要就是一些逻辑的东西,这里就不多说了,说说最后一个问题,那就是,viewPager的setOnPageChangeListener
已经被我们占用了,但是,使用还是想要监听怎么办呢,好吧,又要用到接口回调了
private OnPageChangerListener pageChangerListener;
public void setOnpageChangeListener(OnPageChangerListener pageChangerListener){
this.pageChangerListener = pageChangerListener;
}
public interface OnPageChangerListener{
void onPageSelected(int position);
void onPageScrolled(int position,float positionOffset,int positionOffsetPixels);
void onPageScrollStateChanged(int state);
}
标准的接口回调实现,申明的方法和ViewPager的onPageChangeListener是一样的,
然后再viewPager原本的onPageChangeListener的相应方法中进行调用就好了。
ok,做到这里,一个自定义的ViewPagerIndicator就实现了。
浙公网安备 33010602011771号