Android自定义控件实现导航条IndicatorView

来源:互联网 发布:java多线程简单实例 编辑:程序博客网 时间:2024/06/05 22:58

先上效果图,DEMO在最下面



这个控件其实算是比较轻量级的,相信不少小伙伴都能做出来。因为项目中遇到了一些特殊的定制要求,所以就自己写了一个,这里放出来。

首先来分析下这个控件的功能:

  • 能够响应左右滑动,并且能响应快速滑动
  • 选择项和未选择项有不同的样式表现,比如前景色,背景色,字体大小变粗之内的
  • 在切换选项的时候,如果当前选项未完全呈现在界面前,则自动滚动直至当前选项完全暴露显示
前两条还有,简简单单就实现了,主要是第三点,这才是我自定义这个控件的原因!那么如果要实现这个控件,需要用到哪些知识呢?
  • 用Scroller来实现控件的滚动
  • 用VelocityTracker来实现控件的快速滚动

如果上面两种技术你都已经会了,那么我们就可以开始讲解代码了。首先是一些属性的Getter/Setter方法,这里采用的链式设置法:
    public IndicatorView color(int colorDefault, int colorSelected, int colorBg){        this.colorDefault = colorDefault;        this.colorSelected = colorSelected;        this.colorBg = colorBg;        return this;    }    public IndicatorView textSize(int textSize){        this.textSize = textSize;        return this;    }    public IndicatorView text(String[] texts){        this.texts = texts;        return this;    }    public IndicatorView padding(int[] padding){        this.padding = padding;        return this;    }    public IndicatorView defaultSelect(int defaultSelect){        this.selectItem = defaultSelect;        return this;    }    public IndicatorView lineHeight(int lineHeight){        this.lineHeight = lineHeight;        return this;    }    public IndicatorView listener(OnIndicatorChangedListener listener){        this.listener = listener;        return this;    }    public IndicatorView type(Type type){        this.type = type;        return this;    }

这里我们将每一个选项抽象成了一个Item类:
    public class Item {        String text;        int colorDefault;        int colorSelected;        int textSize;        boolean isSelected = false;        int width;        Point drawPoint;        int[] padding = new int[4];        Rect rect = new Rect();    }

然后是控件的初始化操作,主要根据当前控件的宽高,以及设置的一些属性,进行Item选项的初始化:
    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){        width = MeasureSpec.getSize(widthMeasureSpec);        height = MeasureSpec.getSize(heightMeasureSpec);        //初始化Item        initItems();        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    private void initItems(){        items.clear();        measureWidth = 0;        for(int i = 0; i < texts.length; i++){            Item item = new Item();            item.text = texts[i];            item.colorDefault = colorDefault;            item.colorSelected = colorSelected;            item.textSize = textSize;            for(int j = 0; j < item.padding.length; j++){                item.padding[j] = padding[j];            }            mPaint.setTextSize(item.textSize);            item.width = (int)mPaint.measureText(item.text);            int dx = 0;            if(i - 1 < 0){                dx = 0;            }else{                for(int j = 0; j < i; j++){                    dx += items.get(j).padding[0] + items.get(j).width + items.get(j).padding[2];                }            }            int startX = item.padding[0] + dx;            Paint.FontMetrics metrics =  mPaint.getFontMetrics();            int startY = (int)(height / 2 + (metrics.bottom - metrics.top) / 2 - metrics.bottom);            item.drawPoint = new Point(startX, startY);            //设置区域            item.rect.left = item.drawPoint.x - item.padding[0];            item.rect.top = 0;            item.rect.right = item.drawPoint.x + item.width + item.padding[2];            item.rect.bottom = height;            //设置默认            if(i == selectItem){                item.isSelected = true;            }            measureWidth += item.rect.width();            items.add(item);        }        //重绘        invalidate();    }

接下来是事件处理,逻辑很简单。在DOWN时间记录坐标值,在MOVE中处理控件的滚动,在UP中处理滚动超屏时的恢复操作,以及点击的操作。
    @Override    public boolean onTouchEvent(MotionEvent event){        if(mVelocityTracker == null) {            mVelocityTracker = VelocityTracker.obtain();        }        mVelocityTracker.addMovement(event);        switch(event.getAction()){            case MotionEvent.ACTION_DOWN:                mTouchX = (int)event.getX();                mTouchY = (int)event.getY();                mMoveX = mTouchX;                return true;            case MotionEvent.ACTION_MOVE:                if(measureWidth > width){                    int dx = (int)event.getX() - mMoveX;                    if(dx > 0){ // 右滑                        if(mScroller.getFinalX() > 0){                            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);                        }else{                            mScroller.setFinalX(0);                        }                    }else{ //左滑                        if(mScroller.getFinalX() + width - dx < measureWidth){                            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -dx, 0);                        }else{                            mScroller.setFinalX(measureWidth - width);                        }                    }                    mMoveX = (int)event.getX();                    invalidate();                }                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                if(measureWidth > width){                    mVelocityTracker.computeCurrentVelocity(1000);                    int max = Math.max(Math.abs(mScroller.getCurrX()), Math.abs(measureWidth - width - mScroller.getCurrX()));                    mScroller.fling(mScroller.getFinalX(), mScroller.getFinalY(), (int)-mVelocityTracker.getXVelocity(), (int)-mVelocityTracker.getYVelocity(), 0, max, mScroller.getFinalY(), mScroller.getFinalY());                    //手指抬起时,根据滚动偏移量初始化位置                    if(mScroller.getCurrX() < 0){                        mScroller.abortAnimation();                        mScroller.startScroll(mScroller.getCurrX(), mScroller.getCurrY(), -mScroller.getCurrX(), 0);                    }else if(mScroller.getCurrX() + width > measureWidth){                        mScroller.abortAnimation();                        mScroller.startScroll(mScroller.getCurrX(), mScroller.getCurrY(), measureWidth - width - mScroller.getCurrX(), 0);                    }                }                if(event.getAction() == MotionEvent.ACTION_UP){                    int mUpX = (int)event.getX();                    int mUpY = (int)event.getY();                    //模拟点击操作                    if(Math.abs(mUpX - mTouchX) <= mTouchSlop && Math.abs(mUpY - mTouchY) <= mTouchSlop){                        for(int i = 0; i < items.size(); i++){                            if(items.get(i).rect.contains(mScroller.getCurrX() + mUpX, getScrollY() + mUpY)){                                setSelected(i);                                return super.onTouchEvent(event);                            }                        }                    }                }                break;            default:                break;        }        return super.onTouchEvent(event);    }

接下来就是很重要的一段代码,因为这段代码,才可以让未完全显示的Item选项被选中时自动滚动至完全显示:
    public void setSelected(int position){        if(position >= items.size()){            return;        }        for(int i = 0; i < items.size(); i++){            if(i == position){                items.get(i).isSelected = true;                if(i != selectItem){                    selectItem = i;                    //判断是否需要滑动到完全可见                    if(mScroller.getCurrX() + width < items.get(i).rect.right){                        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), items.get(i).rect.right - mScroller.getCurrX() - width, mScroller.getFinalY());                    }                    if(items.get(i).rect.left < mScroller.getCurrX()){                        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), items.get(i).rect.left - mScroller.getCurrX(), mScroller.getFinalY());                    }                    if(listener != null){                        listener.onChanged(selectItem);                    }                }            }else{                items.get(i).isSelected = false;            }        }        invalidate();    }

然后就是绘制方法了,相当于完全代理给了Item来实现:
    @Override    protected void onDraw(Canvas canvas){        mPaint.setAntiAlias(true);        canvas.drawColor(colorBg);        for(Item item : items){            mPaint.setTextSize(item.textSize);            if(item.isSelected){                if(type == Type.SelectByLine){                    //绘制红线                    mPaint.setColor(item.colorSelected);                    mPaint.setStyle(Paint.Style.FILL);                    canvas.drawRoundRect(new RectF(item.rect.left, item.rect.bottom - lineHeight, item.rect.right, item.rect.bottom), 3, 3, mPaint);                }else if(type == Type.SelectByFill){                    //绘制红色背景                    mPaint.setColor(getContext().getResources().getColor(android.R.color.holo_red_light));                    mPaint.setStyle(Paint.Style.FILL);                    canvas.drawRoundRect(new RectF(item.rect.left + 6, item.rect.top, item.rect.right - 6, item.rect.bottom), item.rect.height() * 5 / 12, item.rect.height() * 5 / 12, mPaint);                }                mPaint.setColor(item.colorSelected);            }else{                mPaint.setColor(item.colorDefault);            }            canvas.drawText(item.text, item.drawPoint.x, item.drawPoint.y, mPaint);        }    }


接下来就是怎么使用这个控件了,布局文件:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/listView"    android:layout_width="match_parent"    android:layout_height="match_parent">    <cc.wxf.androiddemo.indicator.IndicatorView        android:id="@+id/indicator"        android:layout_width="match_parent"        android:layout_height="38dp" /></RelativeLayout>

MainActvity中:
package cc.wxf.androiddemo;import android.content.Context;import android.content.res.Resources;import android.os.Bundle;import android.support.v4.app.FragmentActivity;import cc.wxf.androiddemo.indicator.IndicatorView;public class MainActivity extends FragmentActivity {    private IndicatorView indicatorView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initIndicator();    }    private void initIndicator(){        indicatorView = (IndicatorView)findViewById(R.id.indicator);        Resources resources = getResources();        indicatorView.color(resources.getColor(android.R.color.black),                resources.getColor(android.R.color.holo_red_light),                resources.getColor(android.R.color.darker_gray))                .textSize(sp2px(this, 16))                .padding(new int[]{dip2px(this, 14), dip2px(this, 14), dip2px(this, 14), dip2px(this, 14)})                .text(new String[]{"电视剧","电影","综艺","片花","动漫","娱乐","会员1","会员2","会员3","会员4","会员5","会员6"})                .defaultSelect(0).lineHeight(dip2px(this, 3))                .listener(new IndicatorView.OnIndicatorChangedListener(){                    @Override                    public void onChanged(int position){                    }                }).commit();    }    public static int dip2px(Context context, float dipValue){        final float scale = context.getResources().getDisplayMetrics().density;        return (int)(dipValue * scale + 0.5f);    }    public static int sp2px(Context context, float spValue){        final float scale = context.getResources().getDisplayMetrics().scaledDensity;        return (int)(spValue * scale + 0.5f);    }    @Override    protected void onDestroy() {        super.onDestroy();        indicatorView.release();    }}

因为时间比较晚了,再加上这个东西的确挺简单,因此文章就写得比较草率,大家就将就看吧。
点我下载DEMO


1 0