项目中我们经常会使用viewpager+fragment,这时候就需要一个导航控件,本文介绍如何自定义一个导航控件。
先看效果图:
先看xml布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.example.hp.simpleviewpagerindicator.custom.SimpleIndicator
android:id="@+id/ll_indicator"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary">
</com.example.hp.simpleviewpagerindicator.custom.SimpleIndicator>
<android.support.v4.view.ViewPager
android:id="@+id/vp_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>
</LinearLayout>
布局文件就一个自定义的SimpleIndicator和一个ViewPager用来存放fragment。fragment的代码很简单,就不放了。
接下来看自定的的SimpleIndicator:
public class SimpleIndicator extends LinearLayout {
private String[] titles = new String[]{"番茄","土豆","西红柿"};
private Paint mPaint;
private int mTabCount;
private float mTranslationX = 0;
private int position = 0;
private IndicatorListener mListener;
private SparseArray<TextView> mList = new SparseArray<>(); //存放标题的TextView
public SimpleIndicator(Context context) {
this(context,null);
}
public SimpleIndicator(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
}
//listener监听标题项的点击事件
public void setListener(IndicatorListener listener){
mListener = listener;
}
public void setTitles(String[] titles){
this.titles = titles;
mTabCount = titles.length;
generateTitleView();
}
//添加标题项
private void generateTitleView() {
if(getChildCount()!=0)removeAllViews();
for(int i=0;i<mTabCount;i++){
TextView textView = new TextView(getContext());
textView.setText(titles[i]);
if(i==0){
textView.setTextColor(Color.RED);
}else {
textView.setTextColor(Color.BLACK);
}
textView.setTag(i);
LayoutParams layoutParams = new LayoutParams(0,LayoutParams.MATCH_PARENT,1);
textView.setLayoutParams(layoutParams);
textView.setGravity(Gravity.CENTER);
//设置点击监听,回调listener的方法
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mListener!=null)mListener.onClick((Integer) v.getTag());
}
});
addView(textView);
mList.put(i,textView);
}
}
//更新标题的字体颜色
public void refreshTextColor(int k){
for(int i=0;i<mTabCount;i++){
TextView textView = mList.get(i);
if(i!=k){
textView.setTextColor(Color.BLACK);
}else{
textView.setTextColor(Color.RED);
}
}
position = k;
}
public void vpScroll(int pos, float offset){
mTranslationX = (pos+offset)*getWidth()/mTabCount;
//invalidate方法会执行draw过程,重绘View树。
// View(非容器类)调用invalidate方法只会重绘自身,ViewGroup调用则会重绘整个View树。
invalidate();
int current = Math.round(pos+offset);
if(current!=position)refreshTextColor(current);
}
/**
* View组件的绘制会调用draw(Canvas canvas)方法,draw过程中主要是先画Drawable背景,
* 对drawable调用setBounds()然后是draw(Canvas c)方法.注意的是背景drawable的实际大小会影响view组件的大小
* drawable的实际大小通过getIntrinsicWidth()和getIntrinsicHeight()获取,
* 当背景比较大时view组件大小等于背景drawable的大小
* 画完背景后,draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法,
* dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。
* 值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法
* 当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。
* 因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,
* 或者自定制一个Drawable,重写它的draw(Canvas c)和 getIntrinsicWidth(),
*/
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.drawLine(mTranslationX,getMeasuredHeight()-5,mTranslationX+getMeasuredWidth()/mTabCount,getMeasuredHeight()-5,mPaint);
}
public void setIndicatorListener(IndicatorListener listener){
this.mListener = listener;
}
public interface IndicatorListener{
void onClick(int position);
}
}
这里注意:
①View组件的绘制会调用draw方法,draw过程中主要是先画Drawable背景,对drawable调用setBounds然后是draw犯法。注意的是背景drawable的实际大小会影响view组件的大小,当背景比较大时view组件大小等于背景drawable的大小。
②调用invalidate方法时,draw过程会调用onDraw方法,然后就是dispatchDraw方法,dispatchDraw主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw方法。但是ViewGroup容器组件的绘制,当他没有背景时直接调用的是dispatchDraw方法,而绕过了draw方法,当他有背景的时候就调用draw方法,而draw方法里包含了dispatchDraw方法的调用。因此要在ViewGroup上绘制东西的时候重写的往往是dispatchDraw方法而不是onDraw方法,或者自定制一个Drawable,重写它的draw和getIntrinsicWidth方法。
接下来看MainActivity,MainActivity是用kotlin写的:
class MainActivity : AppCompatActivity() {
var string_array: Array<String> = arrayOf("番茄","土豆","西红柿")
var indicator: SimpleIndicator? = null
var mViewpager: ViewPager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
indicator = findViewById(R.id.ll_indicator)
indicator?.setIndicatorListener(object: SimpleIndicator.IndicatorListener{
override fun onClick(position: Int) {
mViewpager?.currentItem = position
}
})
indicator?.setTitles(string_array)
mViewpager = findViewById(R.id.vp_fragment)
var list: MutableList<Fragment> = mutableListOf()
for(String in string_array){
list.add(TestFragment())
}
var mAdapter: TabFragmentPagerAdapter = TabFragmentPagerAdapter(supportFragmentManager,list)
mViewpager?.adapter = mAdapter
mViewpager?.addOnPageChangeListener(object: ViewPager.OnPageChangeListener{
override fun onPageScrollStateChanged(p0: Int) {
}
/**
* 从第一项滚动到第二项时,滚动前position为0,滚动时position为0,offset由0增到1,滚动完成position为1
* 从第二项滚动到第一项是,滚动前position为1,滚动时position为0,offset由1减到0,滚动完成position为0
*/
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
indicator?.vpScroll(position,positionOffset)
}
override fun onPageSelected(p0: Int) {
}
})
}
}
这里注意:
ViewPager的OnPageChangeListener中的onPageScrolled方法的三个参数,看名字就知道,第一个参数是当前fragment的索引,第二个参数是滑动过程中的偏移量,第三个参数是偏移的像素值。
举个例子:这里有番茄、土豆、西红柿三个标题,也就是三个fragment,
①假设我们当前在第一项,还没开始滑动的时候,position是0,也就是第一项的索引,当我们从第一项(番茄)滑动到第二项(土豆)时,position不变,positionOffset从0增到1,滑动到第二项的时候,position变为1,positionOffset变为0
②假设我们当前在第二项,还没开始滑动的时候,position是1,也就是第二项的索引,当我们从第二项(土豆)滑动到第一项(番茄)时,position瞬间变为0,positionOffset从1减到0,滑动到第二项的时候,position为0,positionOffset变为0
单我们点击标题项时,回调到MainActivity的listener,这里调用ViewPager的setCurrentItem,setCurrentItem还是通过滑动的方法切换fragment,最终还是会调用onPageScrolled方法。
完整代码地址:https://github.com/wuxiaogui593/SimpleViewPagerIndicator