Android自定义时间轴

本文介绍如何在Android中创建自定义时间轴视图,通过分析遇到的问题和解决过程,探讨了onMeasure方法的重要性,指出在宽高设置为包裹内容时,需要重写onMeasure以确保布局正确居中显示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天来分享一下时间轴,最近看很多App里面都很常见,水平一般,所以仿了一个。
先上张图片:
这里写图片描述

代码:
MainActivity:

public class MainActivity extends AppCompatActivity {

    private MyView myView;
    private View activity_main;
    private View top;
    private View bottom;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //通过LayoutInflater动态加载布局,主要取到activity_main,这个布局,然后通过setContentView进行设置,为什么不直接设置setContentView(R.layout.activity_main);
        //因为在onResume之前都无法得到View测量的结果,width和heigh都是0,所以通过addOnGlobalLayoutListener进行回掉(当onMeasure,onLayot执行完毕会回掉,才能拿到宽高)
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        activity_main = layoutInflater.inflate(R.layout.activity_main, null);
        //得到自定义View
        myView = (MyView) activity_main.findViewById(R.id.myView);
        //分别得到两段文字的TextView
        top = activity_main.findViewById(R.id.top);
        bottom = activity_main.findViewById(R.id.bottom);
        setContentView(activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();
        //activity_main布局全部构建完毕会回调
        ViewTreeObserver Observer = activity_main.getViewTreeObserver();
        Observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //移除监听
                activity_main.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                //设置文本
                myView.setText(new String[]{"下","中","上"});
                //分别设置两段文字的高
                myView.setViewHeight(new int[]{bottom.getTop(),top.getTop()+200,top.getTop()});
                //强制View重绘
                myView.mInvalidate();
                myView.setRadius(36);
            }
        });
    }
}

自定义View:

public class MyView extends View {
    //绘制圆的半径
    private int radius;
    //每段文字的高(决定着小球的高)
    private int[] viewHeight;
    //小圆球上面的文本
    private String[] text;

    private Paint p;
    private Rect mBounds;

    public MyView(Context context) {
        super(context);
        init();
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        p = new Paint();
        p.setColor(Color.RED);
        p.setAntiAlias(true);
        mBounds = new Rect();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int  heightsize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(radius*2,heightsize);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    public void setViewHeight(int[] viewHeight) {
        this.viewHeight = viewHeight;
    }

    public void setText(String[] text) {
        this.text = text;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < text.length; i++) {
            p.setColor(Color.RED);
            int height = viewHeight[i];
            //拿到第一个的高,和下一个的高,用作画线,先让下一个的高等于前一个的高,如果下一个要是在数组没有,会报错,所以要捕获.
            int nextheight = height;
            try {
                nextheight = viewHeight[i + 1];
            } catch (Exception e) {

            }
            //拿到当前文字
            String str = text[i];
            //画小球
            canvas.drawCircle(radius, height + radius, radius, p);
            p.setColor(Color.WHITE);
            p.setTextSize(30);
            //这个是得到文字的宽和高
            p.getTextBounds(str, 0, str.length(), mBounds);
            float str1W = mBounds.width();
            float str1H = mBounds.height();
            //画文字
            canvas.drawText(str, radius - str1W / 2, height + radius + str1H / 2, p);
            p.setColor(Color.RED);
            //画线
            canvas.drawLine(radius, height, radius, nextheight, p);
        }
    }

    public void mInvalidate() {
        //重新绘制onMeasure->onLayout->onDraw
        requestLayout();
    }


}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<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="300dp"
    android:orientation="horizontal"
    tools:context="com.example.yaoyan.myapplication.MainActivity">

    <LinearLayout
        android:layout_width="0dp"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:layout_height="match_parent"
        android:layout_weight="1">

        <com.example.yaoyan.myapplication.MyView
            android:id="@+id/myView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <TextView
            android:id="@+id/top"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="邀请微信好友"
            android:textSize="30sp" />

        <TextView
            android:id="@+id/bottom"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="150dp"
            android:text="让好友发送985760到你的微信" />

    </LinearLayout>

</LinearLayout>

这里说一下遇到的坑吧!

<com.example.yaoyan.myapplication.MyView
            android:id="@+id/myView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

我在这里宽和高都设置成为包裹内容,但却发现外层父布局的

 android:gravity="center_horizontal"

失效,后来发现没有重写

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int  heightsize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(radius*2,heightsize);
    }

这个方法
这个方法是测量宽高的,我们来看看调用的super都干了些什么!?

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

setMeasuredDimension 这个方法就设置测量之后的宽高了,那我们来看看getDefaultSize 这个方法干了什么:

  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

这里发现一个问题,不管是MeasureSpec.AT_MOST 还是MeasureSpec.EXACTLY 他最后的返回结果都是specSize,specSize是从MeasureSpec.getSize(measureSpec); 得到的。measureSpec 是传递过来的widthMeasureSpec (只拿宽举例子)那么这个是从哪里来的呢!?
最后发现是从ViewGroup里面传递来的:

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);

调用了getChildMeasureSpec方法:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这里发现

 case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

...

  return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

这里发现,如果父类是一个精确模式MeasureSpec.EXACTLY,如果子类是一个数值那么:

resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;

如果子类是一个LayoutParams.MATCH_PARENT那么:

resultSize = size;
resultMode = MeasureSpec.EXACTLY;

如果子类是一个LayoutParams.WRAP_CONTENT那么:

 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;

最后从上面的代码,再结合下面讲的:

 public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

就可以知道不管是MeasureSpec.AT_MOST还是MeasureSpec.EXACTLY,最后的结果都是父类的宽或者是高,所以我们在xml文件里写 android:gravity=”center_horizontal”没有作用
所以,我们要在onMeasure中重写:宽

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int  heightsize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(radius*2,heightsize);
    }

到这里问题就解决了!
如果你喜欢分享,一起加入群:524727903

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值