Android自定义控件——IndicatorLayout

效果图:

设置显示数目,超出之后可以拖拽和Fling

最常见的使用

说明:重写LinearLayout实现的ViewPager指示器,可设置显示数目,实际数目大于显示数目时可以拖动和Fling。course的颜色,高度都写成了自定义属性,使用起来非常方便。

属性:Indicator
tab_color_normal:tab文字不选中颜色
tab_color_light:tab文字选中颜色
cursor_color:course游标颜色
cursor_height:course游标高度
cursor_offset:course的左右缩进(大于0)

<!--indicator-->
<attr name="tab_color_normal" format="color"/>
<attr name="tab_color_light" format="color"/>
<attr name="cursor_color" format="color"/>
<attr name="cursor_height" format="dimension"/>
<attr name="cursor_offset" format="dimension"/>
<declare-styleable name="Indicator">
      <attr name="tab_color_normal"/>
      <attr name="tab_color_light"/>
      <attr name="cursor_color"/>
      <attr name="cursor_height"/>
      <attr name="cursor_offset"/>
</declare-styleable>

IndicatorLayout.java

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.OverScroller;
import android.widget.TextView;

import com.tuxing.app.R;

/**
 * Created by Mingwei on 2015/12/2.
 */

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class IndicatorLayout extends LinearLayout implements ViewPager.OnPageChangeListener {

    /**
     * 绘制时用的画笔
     */
    private Paint mPaint;
    /**
     * 默认颜色
     */
    private int mCursorColor;
    /**
     * 滚动的指示器的矩形范围
     */
    private Rect mRect = new Rect();
    /**
     * 滚动游标的绘制范围
     */
    private int mL, mR, mT, mB;
    /**
     * 最多可见的游标数
     */
    private int mVisiableTabCount = 0;
    /**
     * 游标高度
     */
    private int mCursorHeight = 6;
    /**
     * tab的宽度
     */
    private int mTabWidth;
    /**
     *
     */
    private int mOffset = 0;
    /**
     * 选中和非选中状态的标签文字颜色
     */
    private int mTabColorNormal;

    private int mTabColorLight;
    /**
     * 水平滚动的距离
     */
    private float mTranslationX;

    /**
     * 判定X 的TouchSlop
     */
    private float mAssessX;

    private float mLastX;

    /**
     * 来处理自定义的点击事件
     */
    private float mMotionX;

    private float mMotionY;

    private long mClickTime;

    Rect mHitRect = new Rect();

    private boolean isDragging;
    /**
     * 最小移动距离
     */
    private int mTouchSlop;
    /**
     * 速度检测
     */
    private VelocityTracker mVelocityTracker;

    private int mMaximumVelocity;

    private int mMinmumVelocity;

    private OverScroller mScroll;

    /**
     * 需要监听ViewPager动作来跟新游标
     */
    private ViewPager mViewPager;

    public OnIndicatorChangeListener mIndicatorListener;

    /**
     * ViewPager变化监听
     */
    public interface OnIndicatorChangeListener {
        void onChanged(int index);
    }

    private int INDICATOR_STATE = STATE_NORMAL;

    public static final int STATE_NORMAL = 1;

    public static final int STATE_ENLARGE = 2;

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

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

    public IndicatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
        mPaint = new Paint();
        mPaint.setColor(mCursorColor);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        //
        mScroll = new OverScroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mMinmumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
    }

    private void initAttr(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.Indicator);
        mTabColorNormal = array.getColor(R.styleable.Indicator_tab_color_normal, Color.BLACK);
        mTabColorLight = array.getColor(R.styleable.Indicator_tab_color_light, Color.WHITE);
        mCursorColor = array.getColor(R.styleable.Indicator_cursor_color, Color.BLACK);
        mCursorHeight = array.getDimensionPixelSize(R.styleable.Indicator_cursor_height, mCursorHeight);
        mOffset = array.getDimensionPixelSize(R.styleable.Indicator_cursor_offset, mOffset);
        array.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        if (count == 0) {
            return;
        }
        mTabWidth = getMeasuredWidth() / mVisiableTabCount;
        for (int i = 0; i < count; i++) {
            TextView view = (TextView) getChildAt(i);
            LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
            params.weight = 0;
            params.width = mTabWidth;
            params.height = getHeight() - mCursorHeight;
            view.setLayoutParams(params);
        }

        mL = 0 + mOffset;
        mT = getHeight() - mCursorHeight;
        mR = mTabWidth - mOffset;
        mB = getHeight();
        mRect = new Rect(mL, mT, mR, mB);
    }


    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        canvas.translate(mTranslationX, 0);
        canvas.drawRect(mRect, mPaint);
        canvas.restore();

    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        initVelocityTracker(event);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mAssessX = event.getX();
                mLastX = event.getX();

                if (!mScroll.isFinished()) {
                    mScroll.abortAnimation();
                }

                mMotionX = event.getX();
                mMotionY = event.getY();
                mClickTime = System.currentTimeMillis();
                return true;
            case MotionEvent.ACTION_MOVE:
                float newX = event.getX();
                float diffX = newX - mAssessX;

                if (!isDragging && Math.abs(diffX) > mTouchSlop) {
                    isDragging = true;
                }
                float diff = newX - mLastX;
                if (isDragging) {
                    scrollBy(-(int) diff, 0);
                    mAssessX = newX;
                }
                mLastX = newX;
                break;
            case MotionEvent.ACTION_UP:
                isDragging = false;
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                float velocity = mVelocityTracker.getXVelocity();
                if (Math.abs(velocity) > mMinmumVelocity) {
                    if (velocity > 0) {
                        flingToLeft(velocity);
                    } else if (velocity < 0) {
                        flingToRight(-velocity);
                    }
                }

                float upX = event.getX();
                float upY = event.getY();
                long tptime = System.currentTimeMillis();

                if (Math.abs(upX - mMotionX) < 10 && Math.abs(upY - mMotionY) < 10 && (tptime - mClickTime) < 400) {
                    int index = getViewIndex(event);
                    if (index >= 0) {
                        int i = (Integer) getChildAt(index).getTag();
                        mIndicatorListener.onChanged(i);
                    }
                }
                recyleVelcityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                isDragging = false;
                recyleVelcityTracker();
                if (!mScroll.isFinished()) {
                    mScroll.abortAnimation();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    public void scroll(int position, float offset) {
        mTranslationX = getWidth() / mVisiableTabCount * (position + offset);
        /**
         * 当tab数大于可见数目的时候,整个容器滚动
         */
        if (getChildCount() > mVisiableTabCount && offset > 0 && (position >= mVisiableTabCount - 2)) {
            if (mVisiableTabCount != 1) {
                /**
                 * 一屏的数量输mVisiableTabCount
                 * 当滚动到倒数第二个是,Indicator不再滚动
                 */
                if (position < getChildCount() - 2) {
                    scrollTo((position - (mVisiableTabCount - 2)) * mTabWidth + (int) (offset * mTabWidth), 0);
                } else {
                    /**
                     * 不在发生滚动,让其停在倒数第mVisiableTabCount的位置,能够显示剩余的tab即可
                     */
                    scrollTo((getChildCount() - 2 - (mVisiableTabCount - 2)) * mTabWidth, 0);
                }
            } else {
                scrollTo(position * mTabWidth + (int) (offset * mTabWidth), 0);
            }
        }
        invalidate();
    }

    public void setViewPager(ViewPager viewPager) {
        mViewPager = viewPager;
        mViewPager.setOnPageChangeListener(this);
    }

    public void setTabs(String[] tabs) {
        if (mVisiableTabCount < 1) {
            throw new IllegalArgumentException("indicator tab count must > 1");
        }
        removeAllViews();
        for (String t : tabs) {
            createChild(t);
        }
        setTabLight(0);
    }

    /**
     * 使用的时候这个方法为必须调用的方法,并且在@link{com.tuxing.app.view.IndicatorLayout#setTabs}之前进行调用
     *
     * @param count 可见的tab数目,必须>1
     */
    public void setVisiableTabCount(int count) {
        mVisiableTabCount = count;
    }


    public void setTabLight(int arg0) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            TextView view = (TextView) getChildAt(i);
            if (i == arg0) {
                view.setTextColor(mTabColorLight);
                if (INDICATOR_STATE == STATE_ENLARGE) {
                    view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
                }

            } else {
                view.setTextColor(mTabColorNormal);
                if (INDICATOR_STATE == STATE_ENLARGE) {
                    view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
                }
            }
        }
    }

    /**
     * 创建子View
     *
     * @param text
     */
    private void createChild(String text) {
        TextView view = new TextView(getContext());
        LayoutParams params = new LayoutParams(getScreenWidth() / mVisiableTabCount, LayoutParams.MATCH_PARENT);
        params.gravity = Gravity.CENTER;
        view.setText(text);
        view.setGravity(Gravity.CENTER);
        view.setTextColor(mTabColorNormal);
        view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
        //view.setOnClickListener(this);
        view.setTag(getChildCount());
        addView(view, params);
    }

    public int getScreenWidth() {
        WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics metrics = new DisplayMetrics();
        windowManager.getDefaultDisplay().getMetrics(metrics);
        return metrics.widthPixels;

    }

    public void setOnIndicatorChangeListener(OnIndicatorChangeListener listener) {
        mIndicatorListener = listener;
    }

    @Override
    public void onPageScrollStateChanged(int arg0) {

    }

    @Override
    public void onPageScrolled(int arg0, float arg1, int arg2) {
        scroll(arg0, arg1);
    }

    @Override
    public void onPageSelected(int arg0) {
        setTabLight(arg0);
        if (mIndicatorListener != null) {
            mIndicatorListener.onChanged(arg0);
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        if (x >= 0 && x <= mTabWidth * (getChildCount() - mVisiableTabCount)) {
            super.scrollTo(x, y);
        }
    }

    private void flingToRight(float v) {
        mScroll.fling(getScrollX(), 0, (int) v, 0, 0, mTabWidth * (getChildCount() - mVisiableTabCount), 0, 0);
        invalidate();
    }

    private void flingToLeft(float v) {
        mScroll.fling(getScrollX(), 0, (int) -v, 0, 0, mTabWidth * (getChildCount() - mVisiableTabCount), 0, 0);
        invalidate();
    }

    @Override
    public void computeScroll() {

        if (mScroll.computeScrollOffset()) {
            scrollTo(mScroll.getCurrX(), 0);
            invalidate();
        }
    }

    private int getViewIndex(MotionEvent event) {
        int x = (int) event.getX() + getScrollX();
        int y = (int) event.getY();
        for (int i = 0; i < getChildCount(); i++) {
            if (getViewByEvent(x, y, getChildAt(i))) {
                return i;
            }
        }
        return -1;
    }

    private boolean getViewByEvent(int x, int y, View v) {
        v.getHitRect(mHitRect);
        return mHitRect.contains(x, y);
    }

    public void setState(int state) {
        INDICATOR_STATE = state;
    }

    /**
     * 初始化
     *
     * @param event
     */
    private void initVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * 回收
     */
    private void recyleVelcityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }
}

使用xml

<com.tuxing.app.view.IndicatorLayout
            xmlns:indicator="http://schemas.android.com/apk/res-auto"
            android:id="@+id/quora_indicator"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_42"
            android:orientation="horizontal"
            indicator:cursor_color="@color/common_botton_bar_blue"
            indicator:cursor_height="2dp"
            indicator:cursor_offset="30dp"
            indicator:tab_color_light="@color/common_botton_bar_blue"
            indicator:tab_color_normal="@color/circle_black"
            android:layout_toLeftOf="@+id/article_resource_indicator_add"
            android:background="@color/white"/>

使用java

String mTabs[] = new String[]{"最新", "热门", "个人"};
mIndicator = (IndicatorLayout) findViewById(R.id.quora_indicator);
//visiableTabCount 表示要显示的tab数目,如果显示小于实际数,则可以拖拽
mIndicator.setVisiableTabCount(mTabs.length);
mIndicator.setTabs(mTabs);
mIndicator.setOnIndicatorChangeListener(this);
mIndicator.setViewPager(mViewPager);

关于我:

/**
 * Created by mingwei on 2/25/16.
 *
 * 微博:      明伟突然(http://weibo.com/u/2382477985)
 * Github:    https://github.com/gumingwei
 * CSDN:      http://blog.csdn.net/u013045971
 * 简书:      http://www.jianshu.com/users/6ad7e8d1e5ae
 * QQ&WX:721881283
 *
 */
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,376评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,345评论 0 17
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,559评论 18 399
  • 玛蒂又一次哭红了鼻子。 她讨厌只会哭的自己,讨厌软弱的自己。 又一个黑夜来袭。 玛蒂裹紧被子蒙住了头,使这黑夜愈发...
    Su萱阅读 229评论 0 1
  • 我并非英语专业的。学英语这么多年,有一个问题一直困惑着我。英语语法怎么就这么难学呢?之前也看过,比如薄冰的英语语...
    丫丫的脚丫阅读 1,332评论 31 32