天天看點

Android自定義時間軸

今天來分享一下時間軸,最近看很多App裡面都很常見,水準一般,是以仿了一個。

先上張圖檔:

Android自定義時間軸

代碼:

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()+,top.getTop()});
                //強制View重繪
                myView.mInvalidate();
                myView.setRadius();
            }
        });
    }
}
           

自定義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*,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 = ; i < text.length; i++) {
            p.setColor(Color.RED);
            int height = viewHeight[i];
            //拿到第一個的高,和下一個的高,用作畫線,先讓下一個的高等于前一個的高,如果下一個要是在數組沒有,會報錯,是以要捕獲.
            int nextheight = height;
            try {
                nextheight = viewHeight[i + ];
            } catch (Exception e) {

            }
            //拿到目前文字
            String str = text[i];
            //畫小球
            canvas.drawCircle(radius, height + radius, radius, p);
            p.setColor(Color.WHITE);
            p.setTextSize();
            //這個是得到文字的寬和高
            p.getTextBounds(str, , str.length(), mBounds);
            float str1W = mBounds.width();
            float str1H = mBounds.height();
            //畫文字
            canvas.drawText(str, radius - str1W / , height + radius + str1H / , 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" />
           

我在這裡寬和高都設定成為包裹内容,但卻發現外層父布局的

失效,後來發現沒有重寫

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int  heightsize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(radius*,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(, specSize - padding);

        int resultSize = ;
        int resultMode = ;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= ) {
                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 >= ) {
                // 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 >= ) {
                // 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 ?  : 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 ?  : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
           

這裡發現

case MeasureSpec.EXACTLY:
            if (childDimension >= ) {
                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*,heightsize);
    }
           

到這裡問題就解決了!

如果你喜歡分享,一起加入群:524727903