1.實際效果
由于之前用到了一個比較老的類似的訓示器架構,發現它功能并不完善(發現顯示層是用圖檔來做的,感覺好呆闆的控件),而且UI很醜(使用藍色png圖檔填充的,為了搭配我的app色調,特意用ps把原圖檔素材全改為白色了[]~( ̄▽ ̄)~*),就如下面我之前做的App上展示的那樣。後來發現Bilibili用戶端的效果不錯,稍微思考一下發現原理不難,就嘗試着自己寫一個,終于成品出來了!
從左到右依次是純粹菜單,Bilibili用戶端,本案例demo
2.原理機制
完成這個自定義ViewGroup時遇到了不少bug,不過主要是邏輯方面的bug,認真檢查後一一排除問題了。
制作類似的控件要解決的主要是滑動ViewPager時訓示器的顯示問題,這個需要很細緻清晰的邏輯,然後是對自定義View各個内部方法調用順序和機制的熟練程度。
整個PagerIndicator運作原理:根據傳入的ViewPager,調用其監聽方法,根據其狀态動态繪制标題下方的小橫條(矩形)。當使用者滑動頁面時,小橫條會按比例移動相應的距離;當滑動結束時,小橫條固定在指定的位置
下面貼出PagerIndicator源碼:
package chen.capton.custom;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import static android.content.ContentValues.TAG;
/**
* Created by CAPTON on 2017/1/13.
*/
public class PagerIndicator extends HorizontalScrollView implements View.OnClickListener{
private Context context;
private int textColor; //标題字型的顔色
private int textCheckedColor; //選中時标題字型的顔色
private int textSize; //标題字型大小
private int lineColor; //橫條顔色
private int lineHeight; //橫條厚度(高度)
private float lineProportion; //橫條占比(寬度)0.0~1.0
private ViewPager viewPager; //儲存傳進來的ViewPager
private List<TextView> textViewList=new ArrayList<>();//顯示各個标題的TextView集合
private List<String> titleList;//标題字元串ArrayList
private LinearLayout wrapper; //由于控件繼承自HorizontalView,其直系子View必須是唯一的LinearLayout。
public ViewPager getViewPager() {return viewPager;}
//傳回值為控件本體對象,友善使用者鍊式調用,下同
public PagerIndicator setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
return this;
}
public List<String> getTitleList() {return titleList;}
public PagerIndicator setTitleList(List<String> titleList) {
this.titleList = titleList;
return this;
}
public int getTextColor() {return textColor;}
public PagerIndicator setTextColor(int textColor) {
this.textColor = getResources().getColor(textColor);
invalidate();
return this;
}
public int getTextSize() {return textSize;}
public PagerIndicator setTextSize(int textSize) {
this.textSize = DisplayUtil.sp2px(context,textSize);;
invalidate();
return this;
}
public int getTextCheckedColor() {return textCheckedColor;}
public PagerIndicator setTextCheckedColor(int textCheckedColor) {
this.textCheckedColor = getResources().getColor(textCheckedColor);
invalidate();
return this;
}
public int getLineColor() {return lineColor;}
public PagerIndicator setLineColor(int lineColor) {
this.lineColor = getResources().getColor(lineColor);
paint.setColor(this.lineColor);
invalidate();
return this;
}
public int getLineHeight() {return lineHeight;}
public PagerIndicator setLineHeight(int lineHeight) {
this.lineHeight = DisplayUtil.dip2px(context,lineHeight);
invalidate();
return this;
}
public float getLineProportion() {return lineProportion;}
public PagerIndicator setLineProportion(float lineProportion) {
this.lineProportion = lineProportion;
invalidate();
return this;
}
public PagerIndicator(Context context) {
this(context,null);
}
public PagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}
public PagerIndicator(Context context, AttributeSet attrs) {
this(context, attrs,);
this.context=context;
TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.PagerIndicator);
textColor=ta.getColor(R.styleable.PagerIndicator_textColor,getResources().getColor(R.color.pager_indicator_black)); //預設标題字型為黑色
textCheckedColor=ta.getColor(R.styleable.PagerIndicator_textCheckedColor,getResources().getColor(android.R.color.white)); //預設選中标題為白色
lineColor=ta.getColor(R.styleable.PagerIndicator_lineColor,getResources().getColor(android.R.color.white)); //預設橫線為白色
textSize= (int) ta.getDimension(R.styleable.PagerIndicator_textSize,); //預設14sp(44px)
lineHeight= (int) ta.getDimension(R.styleable.PagerIndicator_lineHeight,); //預設6dp(18px)
lineProportion=ta.getFloat(R.styleable.PagerIndicator_lineProportion,f); //預設選項寬度的1/3
/*防止使用者輸入參數超出範圍造成顯示錯誤*/
if(lineProportion>){
lineProportion=;
}
if(lineProportion<){
lineProportion=;
}
ta.recycle();
wrapper=new LinearLayout(context);
paint=new Paint();
paint.setColor(lineColor);
}
boolean once;
int width,heigth;//控件寬度,控件高度
int childrenWidth=; //标題視圖的寬度總和
List<Integer> childWidthList=new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
//當使用者設定本空間高度為“wrap_content”時,預設高度設定為40dp,其他情況則是實際高度
heightSize=heightMode==MeasureSpec.AT_MOST?DisplayUtil.dip2px(context,):heightSize;
width=widthSize;
heigth=heightSize;
while (!once) {
LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(widthSize, heightSize);
lp2.gravity=LinearLayout.HORIZONTAL;
wrapper.setLayoutParams(lp2);
//根據傳進來的标題ArrayList動态添加子View(标題項)
for (int i = ; i <titleList.size(); i++) {
int childWidth=titleList.get(i).length()*;
childWidthList.add(childWidth);
TextView textView=new TextView(context);
textView.setText(titleList.get(i));
textView.setTextSize(DisplayUtil.px2sp(context,textSize));
textView.setTextColor(textColor);
textView.setGravity(Gravity.CENTER);
LayoutParams lp3=new LayoutParams(childWidth,
heightSize-DisplayUtil.px2dip(context,lineHeight));
textView.setLayoutParams(lp3);
//設定Tag,用于辨別此TextView對象,當點選标題時,根據此Tag的i值,ViewPager将跳轉至頁面i。
textView.setTag(i);
textView.setOnClickListener(this);
childrenWidth+=childWidth;
textViewList.add(textView);
wrapper.addView(textView);
}
textViewList.get().setTextColor(textCheckedColor);
addView(wrapper);
once=true;
}
setMeasuredDimension(widthSize,heightSize);//測量控件本體
}
int tempPosition=;
float goneWidth=;//儲存目前标題之前的所有标題寬度,通過計算,确定目前小橫條的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
wrapper.layout(,,childrenWidth,heigth);//布局直系唯一子View,參數 r 必須等于其子View的寬度之和,如果等于父控件寬度,則當子View寬度大于父控件時,多出部分的子View沒法顯示,也沒有拖動效果。
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
/*
*核心算法部分,當手指滑動螢幕時頁面跟着滑動,訓示器的小橫條跟着移動;停止滑動時,小橫條顯示在正确的位置上。(ps:這部分算法因人而異,我相信大家可以寫出更簡潔高效的代碼,就不多做注釋了)
*/
if(position==tempPosition) {
left = childWidthList.get(position)*(-lineProportion)/
+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
positionOffset*childWidthList.get(position)+goneWidth;
top = heigth - DisplayUtil.px2dip(context,lineHeight);
bottom = heigth;
}else {
int sum=;
for (int i = ; i < position; i++) {
sum+=childWidthList.get(i);
}
goneWidth=sum;
if(position>tempPosition) {
left = childWidthList.get(position)*(-lineProportion)/+
goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
goneWidth;
}else{
left = childWidthList.get(position)*(-lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
}
/*
*這是當頁面很多,标題寬度很大(超過螢幕了)時,将標明的标題居中顯示的算法
*/
float scrollCenter=goneWidth+childWidthList.get(position)/;
if (scrollCenter>=width/){
smoothScrollTo((int) (scrollCenter-width/),);
}else {
smoothScrollTo(,);
}
tempPosition=position;
}
invalidate();
}
@Override
public void onPageSelected(int position) {
for(TextView textview:textViewList){
textview.setTextColor(textColor);
}
textViewList.get(position).setTextColor(textCheckedColor);
invalidate();
}
@Override
public void onPageScrollStateChanged(int state) {}
});
}
Paint paint; //繪制小橫條的畫筆
float left,top,right,bottom;//繪制小橫條(矩形)的四個邊界
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(left,top,right,bottom,paint);
}
@Override
public void onClick(View v) {
if(v instanceof TextView){
//根據之前動态初始化TextView時設定的Tag(int類型),跳轉頁面
viewPager.setCurrentItem((Integer) v.getTag());
}
}
}
在ViewGroup中,onMeasure,onLayout,onDraw三個方法依次調用,其中onDraw方法在使用者觸摸或者拖動控件時會調用多次,當調用invalidate方法時,onDraw也會被調用。是以有了我們重繪控件的契機,即調用傳進來的ViewPager對象的setOnPageChangeListener(new ViewPager.OnPageChangeListener() {})方法,通過在添加的OnPageChangeListener監聽器中根據ViewPager的滑動狀态來同步重繪我們的PagerIndicator控件的小橫條,例如:
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
/*
*核心算法部分,當手指滑動螢幕時頁面跟着滑動,訓示器的小橫條跟着移動;停止滑動時,小橫條顯示在正确的位置上。(ps:這部分算法因人而異,我相信大家可以寫出更簡潔高效的代碼,就不多做注釋了)
*/
if(position==tempPosition) {
left = childWidthList.get(position)*(-lineProportion)/
+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
positionOffset*childWidthList.get(position)+goneWidth;
top = heigth - DisplayUtil.px2dip(context,lineHeight);
bottom = heigth;
}else {
int sum=;
for (int i = ; i < position; i++) {
sum+=childWidthList.get(i);
}
goneWidth=sum;
if(position>tempPosition) {
left = childWidthList.get(position)*(-lineProportion)/+
goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+
goneWidth;
}else{
left = childWidthList.get(position)*(-lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
right = childWidthList.get(position)*(+lineProportion)/+positionOffset*childWidthList.get(position)+goneWidth;
}
/*
*這是當頁面很多,标題寬度很大(超過螢幕了)時,将標明的标題居中顯示的算法
*/
float scrollCenter=goneWidth+childWidthList.get(position)/;
if (scrollCenter>=width/){
smoothScrollTo((int) (scrollCenter-width/),);
}else {
smoothScrollTo(,);
}
tempPosition=position;
}
invalidate();
}
@Override
public void onPageSelected(int position) {
for(TextView textview:textViewList){
textview.setTextColor(textColor);
}
textViewList.get(position).setTextColor(textCheckedColor);
invalidate();
}
@Override
public void onPageScrollStateChanged(int state) {}
});
}
Paint paint; //繪制小橫條的畫筆
float left,top,right,bottom;//繪制小橫條(矩形)的四個邊界
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(left,top,right,bottom,paint);
}
@Override
public void onClick(View v) {
if(v instanceof TextView){
//根據之前動态初始化TextView時設定的Tag(int類型),跳轉頁面
viewPager.setCurrentItem((Integer) v.getTag());
}
}
}
3.具體用法
詳情請見 5.相關檔案
4.作者的話
對于自定義View與自定義ViewGroup我還不是特别熟悉,很多沒見過的用法和函數都沒深入去涉獵,例如專注界面繪制的Canvas,Paint,Drawable等等。希望看到我文章的大觸們多多指教,對于那些看不懂這片代碼的同學,建議先去掌握好自定義View(ViewGroup)的基礎再來吐槽吧。
5.相關檔案
GitHub連結:https://github.com/Ccapton/pagerIndicator