A little chatter

Custom View as the process of Android program development, the key sign of the early to the middle level promotion, has been a lot of Android programmers to learn the only way, there are many peers in the encounter with custom View after the choice to quit, this has caused a phenomenon, that is, these programmers will only take others made good wheel tinkering, It’s not good to never know how to make a wheel.

Although I don’t recommend everyone repeated wheels, after all, you made is not necessarily better than others, ok also spend a lot of time, delay the project schedule, but know how to make the wheels it certainly is very important, can not isolate, after all, a custom View involves knowledge very much and complicated, only mastered the difficult skill, You can go further and climb higher in mobile development when the bonus period is over. After all, the industry is not the law of the jungle, but the law of nature still exists.

All right, so much, the purpose is not to preach, but to hope that everyone can see their own future road, pursue and learn to choose.

Talk about this custom View

Custom View is actually nothing to say, directly look at the effect can be:

That’s about it, but there are a few things that need to be explained briefly:

  1. You can customize the size of the circle
  2. You can customize the thickness of the connecting wire
  3. You can customize finished and unfinished colors
  4. You can customize the size of the stage text
  5. Click events can be added
  6. Support setting progress line between two circles

(For example, if the number of steps is set to 2.3, drawing points from 0 will have a 30 percent completion line between points 2 and 3)

Basically, custom View is like this, suitable for some short progress cycle of business, such as: short cycle of after-sale business or development process and other progress axis.

Custom View drawing ideas

Every time we do a custom View, we have to get into the habit of thinking about drawing first, The process of customizing View is actually a process of drawing on Canvas by using some algorithms (Bessel curve, etc.) based on certain onMeasure rules and drawing process (onDraw) by using Android Paint and custom attribute values (attrs.xml). So without having to learn the basics of drawing (Paint and Canvas do that for you), composition becomes the key for a programmer to draw custom views.

Composition, seemingly accessible but actually elusive, is a process of creation, and creation is a painful process (genius excepted).

Now back to our custom View, the key questions to draw a horizontal progress axis are:

  1. How do I draw points?
  2. How do I draw a line?
  3. How much space to the left of the first dot?
  4. How much space to the right of the second dot?
  5. How should the width and height of custom measurements be calculated?

Problems are actually, draw point and line Canvas have ready-made method, need not worry, the key is the first point to the left of the white space and the last one on the right side of the white space, because when I started drawing simple think just set aside the circular radius, found the following word than round, wide word beyond interface, so have to consider the width of the word and radius, The width is the width of the phone unless you specify a value. The height is the diameter of the circle plus the height of two words plus the padding.

Now that we have the custom View framework set up, we need to consider how to connect and draw unfinished extension lines:

  1. The line is very simple, as long as the number of steps is greater than 1, and then let the total width minus the width of the left and right blank and the diameter of the beginning and end of the two circles, the rest of the width by the number of points -1 to calculate the length of each connecting line, and then connect the lines.

  2. The idea of the extension line of the unfinished progress is to get the maximum number of steps, and draw a complete line covering the percentage of the length of the connecting line after the circle corresponding to the number of steps.

Of course, consider customizing the View to set up click events and provide interfaces, and click events to determine whether the click point is on the timeline circle and redraw finished and unfinished interfaces, and so on.

This idea may be hard to describe clearly when you write it. If you don’t understand it or get confused, it must be my poor expression. Don’t worry, keep reading, the code will tell you everything.

How the code is implemented

Step 1: Add attrs. XML file to the values folder to store the custom attributes of the View. The main purpose is to set the values of the custom attributes in the layout file, so that developers can set them easily.

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <declare-styleable name="TimeLineView">
        <attr name="textSize" format="dimension"/>
        <attr name="tlradius" format="dimension"/>
        <attr name="lineWidth" format="dimension"/>
        <attr name="CompleteColor" format="color"/>
        <attr name="NoCompleteColor" format="color"/>
    </declare-styleable>
</resources>
Copy the code

If you don’t understand this operation, you have to learn by yourself, mainly to learn the knowledge of custom attributes.

Step 2: start to write custom View code, into the train of thought just now, comments I have written very good (personal think), please read carefully, with just I described the confused train of thought can also.

/ * * *@author cool
 */
public class TimeLineView extends View {
    /* Define the brush */
    private Paint mPaint;
    /* Extension line special brush */
    private Paint exPaint;
    /* Font size */
    private float mTextSize;
    /*
    圆形半径
     */
    private float mRadius;
    /* Line thickness */
    private float mLineWidth;
    /*
    完成的颜色
     */
    private int mCompleteColor;
    /* Unfinished color */
    private int mNoCompleteColor;
    /* The number of steps currently executed starts from 0 */
    private float mStep=0;
    /* * List */ of the text passed in
    private List<String> pointStringList;
    /* * The X coordinates of each point */
    private Float[] pointXArray;
    /* * Text height */
    private float mTextHeight;

    /* Custom node click event */
    OnTimeLineStepClickListener onTimeLineStepClickListener;

    /* A list of circles */
    List<CircleCenter> circleCenterList;

    /* * The length of stages between points */
    float sectionLength;

    /* The utility class used to calculate reserving two decimal places */
    BigDecimal bigDecimal;
    public TimeLineView(Context context) {
        this(context,null);
    }

    public TimeLineView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

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

    /** * initializes the * of the custom attribute@paramAttrs The attribute */ defined in attrs. XML
    private void  initAttr(AttributeSet attrs){
        if(attrs ! =null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.TimeLineView);
            mTextSize = typedArray.getDimension(R.styleable.TimeLineView_textSize, 20);
            mRadius = typedArray.getDimension(R.styleable.TimeLineView_tlradius,20);
            mLineWidth=typedArray.getDimension(R.styleable.TimeLineView_tlradius,5);
            mCompleteColor=typedArray.getColor(R.styleable.TimeLineView_CompleteColor,Color.GREEN);
            mNoCompleteColor=typedArray.getColor(R.styleable.TimeLineView_NoCompleteColor,Color.GRAY);
            // Remember to recycle to prevent memory leakstypedArray.recycle(); }}/** * Initializes a list of brush, font height, passed step text content, and a list of center */
    private void init(a){
        mPaint=new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        mPaint.setStrokeWidth(mLineWidth);
         exPaint=new Paint();
        mTextHeight=mPaint.descent() -mPaint.ascent();
        pointStringList=new ArrayList<>();
        pointStringList.add("Step One");
        pointStringList.add("Step two");
        pointStringList.add("Step three");
        circleCenterList=new ArrayList<>();
    }

    /** * The width of the wrap_content mode is the width of the entire screen regardless of the setting, which can be adjusted by yourself ** The height is twice the height of the font plus the diameter of the circle as the default height, can be adjusted if necessary **@paramWidthMeasureSpec Width measure *@paramHeightMeasureSpec Height scale */
   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int minimumWidth = getSuggestedMinimumWidth();
        final int minimumHeight = getSuggestedMinimumHeight();
        int width = measureWidth(minimumWidth, widthMeasureSpec);
        int height = measureHeight(minimumHeight, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    /** * measure width *@paramDefaultWidth defaultWidth *@paramMeasureSpec Width measure *@returnCalculated width */
    private int measureWidth(int defaultWidth, int measureSpec) {

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);


        switch (specMode) {
            case MeasureSpec.AT_MOST:
                defaultWidth = getWidth();
                break;
            case MeasureSpec.EXACTLY:
                defaultWidth = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
                defaultWidth = Math.max(defaultWidth, specSize);
        }
        return defaultWidth;
    }

    /** * height measurement *@paramDefaultHeight the defaultHeight *@paramMeasureSpec Height measure *@returnCalculated height */
    private int measureHeight(int defaultHeight, int measureSpec) {

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                defaultHeight = (int) (2*mTextHeight+(mRadius*2)) + getPaddingTop() + getPaddingBottom();
                break;
            case MeasureSpec.EXACTLY:
                defaultHeight = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
                defaultHeight = Math.max(defaultHeight, specSize);
                break;
        }
        return defaultHeight;
    }
    * 1. Determine the width of the circle and the text in the first step, and use the width as the left distance of the first step * 2. Determine the circle of the last step and the width of the text, using whichever is the distance to the right of the last step * 3. Take the overall width of the custom component and subtract the left and right distances plus the distance between the two radii, and all that is left is to bisect the distance between the remaining points * 4. Divide the remaining distance evenly and draw a circle. * /
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Check if the number of steps passed is greater than 0. If a List of empty data is passed, the number of steps is not called
        if(pointStringList.size()>0) {// Set it to the finished color first, since the drawing process must be completed first and at least one of the steps passed in is completed
            mPaint.setColor(mCompleteColor);
            initViewsPos();
            // Draw to that point
            int currentStep;
            Draw 30 percent of the completion line from steps 3 to 4
            boolean isDrawExtrasLine=false;
            // Loop the X-axis points to draw
            for(int i=0; i<pointXArray.length; i++){ currentStep= i;// If the current step is larger than the set step, you need to change the line and circle color to unfinished. If you want to draw unfinished color, you may need to draw an extension line, then turn on the draw extension line switch.
                if(currentStep >mStep){
                    mPaint.setColor(mNoCompleteColor);
                    isDrawExtrasLine=true;
                }
                drawCirlceAndText(canvas,pointStringList.get(i),pointXArray[i]);
                // If I is greater than 1, it is necessary to draw a line when there are at least two points
                if(i>=1){
                drawLine(canvas,pointXArray[i-1],pointXArray[i]); }}// Draw extension lines, mainly dealing with the 30 percent of finished color lines between the third and fourth circles with steps like 2.3
            if(isDrawExtrasLine){
                bigDecimal  =new  BigDecimal(mStep);
               float mStepTwoPoint =bigDecimal.setScale(2,  BigDecimal.ROUND_HALF_UP).floatValue();
               // Take the number after the decimal point
               float littleCountFloat=mStepTwoPoint-(int)mStepTwoPoint; drawExtrasLine(canvas,littleCountFloat); }}else{
            mPaint.setColor(Color.RED);
            String noStepsWarnText="Number of steps passed in is 0. Please pass in data again.";
            canvas.drawText(noStepsWarnText,(getWidth()/2)-mPaint.measureText(noStepsWarnText)/2,getHeight() - this.mRadius - 1,mPaint); }}/** * Initializes the position of the component * mainly calculates the position of each circle * and records its center position in the array */
    private void initViewsPos(a){
        // The number of steps passed
        int pointCount= pointStringList.size();
        pointXArray=new Float[pointCount];
        // If the length of the text is greater than the radius, use the length of the text to calculate the starting and ending points
        float startDistance=Math.max(mRadius,mPaint.measureText(pointStringList.get(0)) /2);
        float endDistance=Math.max(mRadius,mPaint.measureText(pointStringList.get(pointStringList.size()-1)) /2);
        // The width of each line minus the left space and the right space divided by the number of points minus one can help thinking legend: *-- *-- *-- *
         sectionLength=(getWidth()-startDistance-endDistance)/(pointCount-1);
         // The loop puts the values of X at the start, end, and intermediate points into an array
        for(int i=0; i<pointCount; i++){if(i==0){
                    pointXArray[i]=startDistance;
                }else if(i==pointCount-1){
                    pointXArray[i]=getWidth()-endDistance;
                }else{ pointXArray[i]=startDistance+sectionLength*i; }}}/** * Draw the circle and the following text *@paramCanvas canvas *@paramText Indicates the text content *@paramX the value of the X-axis */
    private void drawCirlceAndText(Canvas canvas,String text,float x){
        canvas.drawCircle(x,getHeight()- this.mRadius * 2 - 30,mRadius,mPaint);
        // Add the center of each circle to the list
        circleCenterList.add(new CircleCenter(x,getHeight()- this.mRadius * 2 - 30));
        Log.e("mTextHeight:",mTextHeight+"");
        canvas.drawText(text,x-mPaint.measureText(text)/2,getHeight() - this.mRadius - 1,mPaint);
    }

    /** * Draw the total number of connecting lines between two circles as points -1 *@paramCanvas canvas *@paramStartX the value of the X-axis at the beginning of line X *@paramThe value of the X-axis at the end of the endX line */
    private void drawLine(Canvas canvas,float startX,float endX){
        canvas.drawLine(startX+mRadius,getHeight()- this.mRadius * 2 - 30,endX-mRadius,getHeight()- this.mRadius * 2 - 30,mPaint);

    }

    /** * Draw extra decimal line segments * The principle here is to calculate the percentage and find the value of the number of completed steps and after that draw a complete color line whose length is one line segment times the percentage of completed *@paramCanvas canvas *@paramPercent Percent completed */
    private void drawExtrasLine(Canvas canvas,float percent){
        // Find the circular X axis with the maximum number of steps
       float maxGreenPointX= pointXArray[(int)mStep];
       // Set the color and width of the special Paint for the extension cord
       exPaint.setColor(mCompleteColor);
       exPaint.setStrokeWidth(mLineWidth);
       canvas.drawLine(maxGreenPointX+mRadius,getHeight()- this.mRadius * 2 - 30,maxGreenPointX+mRadius+sectionLength*percent,getHeight()- this.mRadius * 2 - 30,exPaint);

    }

    /** ** ** ** *@paramMpointStringArray An array of passed steps (array is used here because it can be used@SizeAnnotations) *@paramStep The number of completed steps passed */
    public void setPointStrings(@Size(min = 2) String[] mpointStringArray, @FloatRange(from = 1.0) float step) {
        if (mpointStringArray.length==0) {
            pointStringList.clear();
            circleCenterList.clear();
            mStep = 0;
        } else{ pointStringList = Arrays.asList(mpointStringArray); mStep = Math.min(step, pointStringList.size()); invalidate(); }}/** * Dynamically set the number of steps *@paramStep count * /
    public void setStep(@FloatRange(from = 1.0) float step){
        mStep = Math.min(step, pointStringList.size());
        invalidate();
    }

    /** * Gesture method overwrite returns true to consume this method works by calculating whether the click point is within a certain range of the step circle when the finger is raised. If so, the custom click event is triggered@paramEvent Click the event *@returnTrue Consume event */
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x =  event.getX();
        float y =  event.getY();
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (x + getLeft() < getRight() && y + getTop() < getBottom()) {
                    int clickStep=isInTheCircles(x,y);
                    if(clickStep>=0&&onTimeLineStepClickListener! =null)
                    onTimeLineStepClickListener.onStepClick(clickStep);
                }
                break;
        }
        return true;

    }

    /** Determine if the click position is within one of the circles *@paramX Click on the X-axis of the event *@paramY Click on the Y-axis of the event *@returnBoolean inside a circle: true, false */
    private int isInTheCircles(float x,float y){
        int clickStep=-1;
        for(int i=0; i<circleCenterList.size(); i++){ CircleCenter circleCenter=circleCenterList.get(i);// Click the distance between position x and center x
            float distanceX = Math.abs(circleCenter.getX()-x);
            // Click the distance between position y and center y
            float distanceY = Math.abs(circleCenter.getY()-y);
            // Click the straight-line distance from the center of the circle
            int distanceZ = (int) Math.sqrt(Math.pow(distanceX,2)+Math.pow(distanceY,2));
            If the distance between the click position and the center of the circle is less than or equal to the radius of the circle, the click position is inside the circle
            if(distanceZ <= mRadius){
                 clickStep=i;
                 break; }}return clickStep;
    }

    /* The method that sets the interface is provided externally by calling */
    public void setOnTimeLineStepChangeListener(OnTimeLineStepClickListener onTimeLineStepClickListener){
        this.onTimeLineStepClickListener=onTimeLineStepClickListener;
    }

    /** * Click the interface definition of the event */
    public interface OnTimeLineStepClickListener {

        void onStepClick(float step);
    }

    /** * The inner entity class in the center of the circle * is mainly the Bean that determines whether the click event is provided on the circle */
    class CircleCenter{
        float x;
        float y;

        private CircleCenter(float x,float y){
            this.x=x;
            this.y=y;
        }
        private float getX(a) {
            return x;
        }

        public void setX(float x) {
            this.x = x;
        }

        private float getY(a) {
            return y;
        }

        public void setY(float y) {
            this.y = y; }}}Copy the code

Step 3: The layout file has nothing to say

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    <com.jasoncool.study.view.TimeLineView
        android:id="@+id/timeline1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        app:textSize="20sp"
        app:tlradius="7dp"
        app:CompleteColor="@color/colorAccent"
        app:NoCompleteColor="@color/colorPrimaryDark"
        />
    <com.jasoncool.study.view.TimeLineView
        android:id="@+id/timeline2"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_margin="20dp"
        app:textSize="12sp"
        app:tlradius="5dp"
        />
</LinearLayout>
Copy the code

Step 4: Not much to say about how to use the component introduces ButterKnife, if you don’t use it, you can modify it yourself.

public class TimeLineViewActivity extends AppCompatActivity {

    @BindView(R.id.timeline1)
    TimeLineView timeline1;
    @BindView(R.id.timeline2)
    TimeLineView timeline2;

    public static void goTimeLineViewActivity(Context context) {
        Intent intent = new Intent(context, TimeLineViewActivity.class);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_time_line_layout);
        ButterKnife.bind(this);
        String[] steps = new String[]{"Launch after sale"."Merchant receiving goods"."Merchant Maintenance"."Send back to customer"};
        timeline1.setPointStrings(steps, 1.535 f);
        timeline1.setOnTimeLineStepChangeListener(new TimeLineView.OnTimeLineStepClickListener() {
            @Override
            public void onStepClick(float step) {
                Toast.makeText(TimeLineViewActivity.this."Click on number one" + (int) (step + 1) + "Step", Toast.LENGTH_SHORT).show(); timeline1.setStep(step); }}); String[] steps2 =new String[]{"Step one, step one."."Step two"."Part three, Step three."."Step four"};
        timeline2.setPointStrings(steps2,1.423 f); }}Copy the code

conclusion

The code was not difficult. I spent one morning to write a rudimentary DEMO that could complete the basic requirements. Then I spent another afternoon to optimize the code, improve some ideas and test some extreme cases. The reason why I wrote this is actually to tell you, do not eat fat, custom View must first write basic code according to the idea, and then step by step optimization, of course, if you are familiar with it, it is another matter.

Thanks for watching it!