Android Advanced Drawing - Custom View Fully Mastered

Keywords: Java Android ButterKnife xml encoding

In the previous case, we used some controls of the system to generate our custom controls by combining. The implementation of the custom controls can also be accomplished by inheriting View from the custom class. Beginning with this blog, we inherited View through a custom class to implement some of our custom controls.
We learn through a case, and now we can achieve such an effect.

We create a new class MyToggleButton to inherit View.
Note that you must override the constructor with two parameters, because if we use this class in the layout file, it will be used to instantiate the class, and if not it will crash.
Describes the main methods of creating and displaying a control.

  1. Execution Constructor Instance Class
  2. Measurements, through the measure method, need to rewrite the onMeasure method
    If it is currently a ViewGroup, it has an obligation to measure its children.
    The child has only the right to recommend, that is to say, the child can recommend how tall and wide the control is, and finally the father must decide the width.
  3. The onLayout method needs to be overridden by the layout method.
    Specify the location of the control. In general, View does not need to override this method. It is only when ViewGroup is used that it needs to be overridden.
  4. Drawing a view requires rewriting the onDraw method through the draw method
    Drawing based on some parameters of the above two methods

So we usually only need to rewrite onMeasure(int,int) method and onDraw(canvas) method to customize View.
The basic operation is completed by three methods: measure(), layou() and draw(), which contain onMeasure(), onLayout() and onDraw() respectively.
Post the code for the MyToggleButton class.

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View {

    private Bitmap backgroundBitmap;

    private Bitmap slidingBitmap;

    private int slidLeftMax;
    private Paint paint;

    /**
     *  If we use this class in the layout file, we will use this constructor to instantiate the class, and if not crash.
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//Setting Anti-aliasing
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();
    }

    /**
     * Measurement of Views
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(),backgroundBitmap.getHeight());
    }

    /**
     * Draw
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap,0,0,paint);
        canvas.drawBitmap(slidingBitmap,0,0,paint);
    }
}

Through the above description, I believe you all understand the code. Such a custom View is drawn, and then we use it in the activity_main.xml file.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="match_parent"
    tools:context="com.itcast.test0430_2.MainActivity">

    <com.itcast.test0430_2.MyToggleButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>

Run the project and preview the effect.

Such a static switch has been drawn, and now we have to let the switch change state by clicking.
Let's start with an analysis. The current state is closed. How can we make it open? When we draw the second picture, the left margin is 0, and at this point we have calculated that the opening state needs the left margin, so we just need to modify it like this.

canvas.drawBitmap(slidingBitmap,slidLeftMax,0,paint);

Okay, let's re-run the project and preview the effect.

This makes the switch on. In this case, we can indirectly control the switch state by dynamically changing the value of the left margin.
We reworked the code of the MyToggleButton class.

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * Maximum distance to the left
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * If we use this class in the layout file, we will use this constructor to instantiate the class, and if not crash.
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//Setting Anti-aliasing
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * Measurement of Views
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * Draw
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;

    @Override
    public void onClick(View v) {
        isOpen = !isOpen;

        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //Forced Drawing
        invalidate();//This method results in onDraw() method execution
    }
}

So we finished clicking, running the project, and previewing the effect.

However, this is still a little far from our goal, we continue to achieve the next requirement, switch sliding.
To achieve this requirement, we need to rewrite the onTouchEvent() method to listen for touch events, and then get the coordinates when pressed. But in the event object, there are getX() method and getRawX() method, so which method should we use? What's the difference between the two methods?
I posted two pictures.

I believe you can see the picture at a glance.
We modified the code of the MyToggleButton class.

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * Maximum distance to the left
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * If we use this class in the layout file, we will use this constructor to instantiate the class, and if not crash.
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//Setting Anti-aliasing
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * Measurement of Views
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * Draw
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;

    @Override
    public void onClick(View v) {
        isOpen = !isOpen;

        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //Forced Drawing
        invalidate();//This method results in onDraw() method execution
    }

    private float startX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //1. Record the coordinates pressed
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //2. Record End Value
                float endX = event.getX();
                //3. Calculating offset
                float distanceX = endX - startX;

                slideLeft += distanceX;
                //4. Shielding Illegal Values

                //5. Refreshing
                invalidate();
                //6. Data Restoration
                startX = event.getX();

                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}

Now run the project and preview the effect.

It will be found that the switch has been slipped out. Obviously, this phenomenon is not allowed. We will implement the fourth step of shielding the illegal value.

 if(slideLeft < 0){
       slideLeft = 0;
 }else if(slideLeft > slidLeftMax){
      slideLeft = slidLeftMax;
 }

Now run the preview.

Now we can't slide the switch out of the control, but I don't know if you have noticed that it can slide to an awkward place, either open or closed, but in the middle of the two. That situation is also not allowed, so we can solve it now. Here's the question.
Re-modify the code of the MyToggleButton class.

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * Maximum distance to the left
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * If we use this class in the layout file, we will use this constructor to instantiate the class, and if not crash.
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//Setting Anti-aliasing
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * Measurement of Views
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * Draw
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;

    @Override
    public void onClick(View v) {
        isOpen = !isOpen;

        flushView();
    }

    private void flushView() {
        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //Forced Drawing
        invalidate();//This method results in onDraw() method execution
    }

    private float startX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //1. Record the coordinates pressed
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //2. Record End Value
                float endX = event.getX();
                //3. Calculating offset
                float distanceX = endX - startX;

                slideLeft += distanceX;

                //4. Shielding Illegal Values
                if(slideLeft < 0){
                    slideLeft = 0;
                }else if(slideLeft > slidLeftMax){
                    slideLeft = slidLeftMax;
                }

                //5. Refreshing
                invalidate();
                //6. Data Restoration
                startX = event.getX();

                break;
            case MotionEvent.ACTION_UP:
                if(slideLeft > slidLeftMax / 2){
                    //Display button on
                    isOpen = true;
                }else{
                    isOpen = false;
                }

                flushView();
                break;
        }
        return true;
    }
}

Run the project and preview the effect.

At this time, although there will be no embarrassment of the last time, but here is another problem. When I slide, it always runs in the opposite direction of my slide. I want it to slide to the right, but it will go to the left, which is obviously not possible. This is due to the simultaneous effects of touch events and click events. Now we can solve this problem.
Modify the code of the MyToggleButton class again.

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * Maximum distance to the left
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * If we use this class in the layout file, we will use this constructor to instantiate the class, and if not crash.
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//Setting Anti-aliasing
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * Measurement of Views
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * Draw
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;
    /**
     * true: Click event is valid, slide event is not valid
     * false: Click event is not valid, slide event is valid
     */
    private boolean isEnableClick = true;

    @Override
    public void onClick(View v) {
        if (isEnableClick) {
            isOpen = !isOpen;
            flushView();
        }
    }

    private void flushView() {
        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //Forced Drawing
        invalidate();//This method results in onDraw() method execution
    }

    private float startX;
    private float lastX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //1. Record the coordinates pressed
                lastX = startX = event.getX();
                isEnableClick = true;
                break;
            case MotionEvent.ACTION_MOVE:
                //2. Record End Value
                float endX = event.getX();
                //3. Calculating offset
                float distanceX = endX - startX;

                slideLeft += distanceX;

                //4. Shielding Illegal Values
                if (slideLeft < 0) {
                    slideLeft = 0;
                } else if (slideLeft > slidLeftMax) {
                    slideLeft = slidLeftMax;
                }

                //5. Refreshing
                invalidate();
                //6. Data Restoration
                startX = event.getX();

                if (Math.abs(endX - lastX) > 5) {
                    //Slide
                    isEnableClick = false;
                }

                break;
            case MotionEvent.ACTION_UP:
                if (!isEnableClick) {
                    if (slideLeft > slidLeftMax / 2) {
                        //Display button on
                        isOpen = true;
                    } else {
                        isOpen = false;
                    }
                    flushView();
                }
                break;
        }
        return true;
    }
}

This is our final version of the code, which completes the whole case.

Click Download Source Code

Posted by simonsays on Sun, 25 Aug 2019 21:40:56 -0700