Android 自定义View——自定义一个文本选择框

项目地址:项目地址包含之前的内容
Android 自定义View——自定义一个文本选择框_第1张图片
这种效果也算是比较常用的选择方式了。

  • View的绘制流程
  • 自定义View代码示例

View的绘制流程

//DecorView将会调用
07-10 11:33:18.657 23998-23998/com.example.study E/CustomFrameLayout: requestLayout
07-10 11:33:18.657 23998-23998/com.example.study E/CustomFrameLayout: requestLayout
07-10 11:33:18.657 23998-23998/com.example.study E/CustomFrameLayout: CustomFrameLayout
07-10 11:33:18.657 23998-23998/com.example.study E/CustomButton: CustomButton2
07-10 11:33:18.657 23998-23998/com.example.study E/CustomFrameLayout: requestLayout
07-10 11:33:18.657 23998-23998/com.example.study E/CustomFrameLayout: requestLayout

//CustomFrameLayout的onMeasure调用
07-10 11:33:18.677 23998-23998/com.example.study E/CustomButton: onMeasure
07-10 11:33:18.677 23998-23998/com.example.study E/CustomFrameLayout: onMeasure

//CustomFrameLayout的onLayout调用CustomButton的layout,然后由CustomButton的layout调用onLayout
07-10 11:33:18.707 23998-23998/com.example.study E/CustomButton: onLayout
07-10 11:33:18.707 23998-23998/com.example.study E/CustomButton: layout
07-10 11:33:18.707 23998-23998/com.example.study E/CustomFrameLayout: onLayout

//CustomFrameLayout的onDraw调用CustomButton的onDraw
07-10 11:33:18.707 23998-23998/com.example.study E/CustomFrameLayout: onDraw
07-10 11:33:18.707 23998-23998/com.example.study E/CustomButton: onDraw
07-10 11:33:18.707 23998-23998/com.example.study E/CustomButton: onDraw

//CustomFrameLayout的onMeasure调用CustomButton的onMeasure
07-10 11:33:18.737 23998-23998/com.example.study E/CustomButton: onMeasure
07-10 11:33:18.737 23998-23998/com.example.study E/CustomFrameLayout: onMeasure
07-10 11:33:18.737 23998-23998/com.example.study E/CustomButton: onLayout
07-10 11:33:18.737 23998-23998/com.example.study E/CustomButton: layout
07-10 11:33:18.737 23998-23998/com.example.study E/CustomFrameLayout: onLayout
07-10 11:33:18.737 23998-23998/com.example.study E/CustomFrameLayout: onDraw
07-10 11:33:18.737 23998-23998/com.example.study E/CustomButton: onDraw

CustomFrameLayout继承自FrameLayout,CustomButton继承自Button。而且只有CustomButton是CustomFrameLayout子view。

ViewGroup中的onMeasure、onLayout、onDraw。会遍历自己的子view分别调用onMeasure、layout、onDraw方法。

自定义View代码示例

public class CustomView extends View {
    private static final String TAG = "CustomView";

    /**
     * 文字之间的间隔
     */
    private final int TEXT_LINE_SPACE = 40;

    /**
     * 文字和线之间的间隔
     */
    private final int LINE_SPACE = TEXT_LINE_SPACE / 2;
    /**
     * 默认的文字大小
     */
    private final float DEFAULT_TEXT_SIZE = 32;

    /**
     * 标题
     */
    private List<String> mTitles;
    /**
     * 画线的画笔
     */
    private Paint mLinePaint;

    /**
     * 画标题的画笔
     */
    private Paint mTitlePaint;

    /**
     * view的中心点
     */
    int mCenterX;
    int mCenterY;

    /**
     * 文本中心点初始值Y
     */
    int mCenterTextDefalutY;

    /**
     * 文本的中心点Y
     */
    int mCenterTextY;

    /**
     * 当前文本的下标
     */
    private int currentIndex;
    /**
     * 文本高度
     */
    private int titleHeight;
    /**
     * 文本和行间距的高度
     */
    private int textTotalHeight;

    /**
     * 是否允许滑动超出范围
     */
    private boolean disAllowOutTopOrBottom = true;

    /**
     * 当前手指所在的坐标
     */
    float x;
    float y;


    /**
     * @param context
     */
    public CustomView(Context context) {
        this(context, null);
    }

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

    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mTitles = new ArrayList<>();
        for (int i = 0; i < 12; i++) {
            mTitles.add("第 " + i + " 个标题");
        }

        //划线的画笔
        mLinePaint = new Paint();

        mLinePaint.setColor(Color.GRAY);
        //画标题的画笔
        mTitlePaint = new Paint();
        mTitlePaint.setTextSize(DEFAULT_TEXT_SIZE);
        mTitlePaint.setTextAlign(Paint.Align.CENTER);

        //文本高度
        titleHeight = (int) DEFAULT_TEXT_SIZE;
        //文本和行间距高度
        textTotalHeight = titleHeight + TEXT_LINE_SPACE;

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
        mCenterX = viewWidth / 2;
        mCenterY = viewHeight / 2;

        mCenterTextDefalutY = mCenterY;

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e(TAG, "onMeasure");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.e(TAG, "onLayout");
        super.onLayout(changed, left, top, right, bottom);

    }

    @Override
    public void layout(int l, int t, int r, int b) {
        Log.e(TAG, "layout");
        super.layout(l, t, r, b);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        Log.e(TAG, "onDraw");
        super.onDraw(canvas);

        //没有数据就返回
        if (mTitles.size() == 0) {
            return;
        }
        if (currentIndex < 0) {
            currentIndex = 0;
        }
        if (currentIndex >= mTitles.size()) {
            currentIndex = mTitles.size() - 1;
        }


        //整体的偏移量
        int offset = mCenterY - mCenterTextDefalutY;
        //偏移了多少个文本的高度

        int i = offset / textTotalHeight;

        int i1 = offset % (textTotalHeight);
        //如果超过余下的,大于一个高度,就加一
        if (i1 > textTotalHeight / 2) {
            i++;
        }
        currentIndex = i;
        Log.e("index", "currentIndex:  " + currentIndex);


        mCenterTextY = mCenterTextDefalutY;
        for (String title : mTitles) {
            int index = mTitles.indexOf(title);
            if (index == currentIndex) {
                //当前文本设置颜色
                mTitlePaint.setColor(Color.BLACK);
            } else {
                //其它文本设置颜色
                mTitlePaint.setColor(Color.GRAY);
            }
            canvas.drawText(title, mCenterX, mCenterTextY + (float) titleHeight / 4, mTitlePaint);
            if (title.equals(mTitles.get(mTitles.size() - 1))) {
                break;
            }
            mCenterTextY += titleHeight + TEXT_LINE_SPACE;
        }
        //上面的线
        drawTopLine(canvas, titleHeight);

        drawBottomLine(canvas, titleHeight);
    }

    /**
     * 绘制上方的横线
     *
     * @param canvas
     * @param titleHeight
     */
    private void drawTopLine(Canvas canvas, float titleHeight) {
        float startX = 0;
        float startY = mCenterY - titleHeight / 2 - LINE_SPACE;
        float stopX = mCenterX * 2;
        canvas.drawLine(startX, startY, stopX, startY, mLinePaint);
    }

    /**
     * 绘制下方的横线
     *
     * @param canvas
     * @param titleHeight
     */
    private void drawBottomLine(Canvas canvas, float titleHeight) {
        float startX = 0;
        float startY = mCenterY + titleHeight / 2 + LINE_SPACE;
        float stopX = mCenterX * 2;
        canvas.drawLine(startX, startY, stopX, startY, mLinePaint);
    }

    /**
     * 获取文本区域所在的矩形区域,帮助获取绘制范围宽高
     *
     * @param title
     * @return
     */
    private Rect getTextBounds(String title) {
        Rect bounds = new Rect();
        mTitlePaint.getTextBounds(title, 0, title.length(), bounds);
        return bounds;
    }

    private int getTextWidth(String title) {
        Rect textBounds = getTextBounds(title);
        return textBounds.right - textBounds.left;
    }

    private int getTextHeight(String title) {
        Rect textBounds = getTextBounds(title);
        return textBounds.bottom - textBounds.top;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent" + "  action: " + event.getAction());
        return super.dispatchTouchEvent(event);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent");

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            x = event.getX();
            y = event.getY();
            return true;
        }
        if (event.getAction() == MotionEvent.ACTION_MOVE) {
            //和上次比划过的距离
            float v = event.getY() - y;
            y = event.getY();
            if (checkIsAllowOut(v)) {
                return true;
            }
            mCenterTextDefalutY += v;
            invalidate();
            return true;
        }
        if (event.getAction() == MotionEvent.ACTION_UP) {
            //防止滑出了第一条
            if (mCenterTextDefalutY - mCenterY >= 0) {
                //默认的初始文本中心和view中心相同或者大于就是在第一条
                mCenterTextDefalutY = mCenterY;
                invalidate();
                return true;
            }
            //防止滑出了最后一条
            if (mCenterTextY - mCenterY <= 0) {
                mCenterTextDefalutY = mCenterY - ((mTitles.size() - 1) * textTotalHeight);
                invalidate();
                return true;
            }


            mCenterTextDefalutY = mCenterY - (currentIndex * textTotalHeight);
            invalidate();


            return true;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 检查是否允许超出顶部和底部
     *
     * @param v
     * @return
     */
    private boolean checkIsAllowOut(float v) {
        if (!disAllowOutTopOrBottom) {
            return false;
        }
        if (v > 0) {
            //已经显示的是第一条,禁止下滑
            if (mCenterTextDefalutY - mCenterY >= 0) {
                //默认的初始文本中心和view中心相同或者大于就是在第一条
                mCenterTextDefalutY = mCenterY;
                invalidate();
                return true;
            }
        }

        if (v < 0) {
            //已经显示的是最后一条,禁止上滑
            Log.e(TAG, "mCenterTextY:  " + mCenterTextY);
            Log.e(TAG, "mCenterY:  " + mCenterY);
            Log.e(TAG, "mCenterTextDefalutY:  " + mCenterTextDefalutY);
            if (mCenterTextY - mCenterY <= 0) {
                mCenterTextDefalutY = mCenterY - ((mTitles.size() - 1) * textTotalHeight);
                invalidate();
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean performClick() {
        Log.e(TAG, "performClick");
        return super.performClick();
    }
}

整体思路:

  1. 确定view的中心位置。mCenterY
  2. 根据view中心位置mCenterTextDefalutY ,确定第一个文本的中心位置。然后通过确定的文本高度和行间距,一次往下画出其它文本。
  3. 根据view的中心位置和文本高度确定两天线的位置。
  4. 事件处理,计算滑动距离,改变mCenterTextDefalutY (第一个文本中心位置)的值。
  5. 一些极限处理。
  6. 根据偏移的距离得到偏移了多少个高度,得到当前应该显示的正确位置。在UP的时候要进行检查。

其它知识点、Paint、canvas等。

你可能感兴趣的:(Android进阶,面试,Android,自定义view,文本选择器)