开源 java android app 开发(十三)绘图定义控件、摇杆控件的制作

发布于:2025-06-29 ⋅ 阅读:(19) ⋅ 点赞:(0)

 文章的目的为了记录使用java 进行android app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

 相关链接:

开源 java android app 开发(一)开发环境的搭建-CSDN博客

开源 java android app 开发(二)工程文件结构-CSDN博客

开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客

开源 java android app 开发(四)GUI界面重要组件-CSDN博客

开源 java android app 开发(五)文件和数据库存储-CSDN博客

开源 java android app 开发(六)多媒体使用-CSDN博客

开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客

开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客

开源 java android app 开发(九)后台之线程和服务-CSDN博客

开源 java android app 开发(十)广播机制-CSDN博客

开源 java android app 开发(十一)调试、发布-CSDN博客

开源 java android app 开发(十二)封库.aar-CSDN博客

推荐链接:

开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客

开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客

开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客

开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客

开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客

本章节主要内容是如何进行绘图并自定义控件。在Android开发中,经常需要自定义控件来实现特殊功能,本章主要讲如何自定义一个摇杆控件,通过拖动中间的圆圈,实现上下左右位置的输出,可以用到机器人或无人机的控制中。

1.绘图基础

2.摇杆控件的制作

3.效果图

一、绘图

在Android中使用Java进行绘图主要涉及以下几个核心类和概念:

1.1  Canvas类是Android绘图的基础,提供了各种绘制方法,以下为代码

public class CustomView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        // 设置画笔
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(5);
        
        // 绘制矩形
        canvas.drawRect(100, 100, 300, 300, paint);
        
        // 绘制圆形
        paint.setColor(Color.BLUE);
        canvas.drawCircle(200, 200, 50, paint);
        
        // 绘制文本
        paint.setColor(Color.BLACK);
        paint.setTextSize(40);
        canvas.drawText("Hello Android", 50, 50, paint);
    }
}

1.2  Paint类控制绘图的样式和颜色,以下为代码
 

Paint paint = new Paint();
paint.setColor(Color.GREEN);          // 设置颜色
paint.setStyle(Paint.Style.STROKE);   // 设置填充样式(STROKE, FILL, FILL_AND_STROKE)
paint.setStrokeWidth(10);             // 设置线条宽度
paint.setAntiAlias(true);             // 开启抗锯齿
paint.setTextSize(30);                // 设置文本大小

1.3  绘制基本图形,以下为具体代码

// 绘制直线
canvas.drawLine(startX, startY, endX, endY, paint);

// 绘制矩形
canvas.drawRect(left, top, right, bottom, paint);

// 绘制圆角矩形
canvas.drawRoundRect(left, top, right, bottom, rx, ry, paint);

// 绘制圆形
canvas.drawCircle(centerX, centerY, radius, paint);

// 绘制椭圆
canvas.drawOval(left, top, right, bottom, paint);

// 绘制弧形
canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);

// 绘制路径
Path path = new Path();
path.moveTo(x1, y1);
path.lineTo(x2, y2);
path.quadTo(controlX, controlY, endX, endY);
canvas.drawPath(path, paint);

1.4  创建自定义View,自定义控件可以直接继承自View,以下为典型代码
 

public class MyCustomView extends View {
    private Paint paint;
    private Path path;

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

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setAntiAlias(true);
        
        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(x, y);
                return true;
            case MotionEvent.ACTION_MOVE:
                path.lineTo(x, y);
                break;
            default:
                return false;
        }
        
        // 重绘视图
        invalidate();
        return true;
    }
}

二、遥杆控件的制作,在app开发中经常需要使用到摇杆控件,比如机器人控制,比如无人机的控制,以下为摇杆控件的具体制作方法。

2.1  创建基础类继承自view,JoystickView.java。主要实现摇杆控件的绘制和事件定义,摇杆通常外围会有底座,中间会有小圆圈代表摇杆,中间用三角形来代表方向。拉动时触发事件,提供回调接口供调用者使用。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class JoystickView extends View {


    // 绘制参数
    private Paint basePaint;        // 底座画笔
    private Paint outerRingPaint;   // 外圈浅灰色圆环
    private Paint middleRingPaint;  // 中间白色圆环
    private Paint trianglePaint;    // 三角形画笔
    private PointF centerPoint;     // 中心点
    private PointF stickPoint;      // 摇杆当前位置
    private float baseRadius;       // 底座半径
    private float stickRadius;      // 摇杆半径
    private float maxDistance;      // 最大移动距离
    private Path upTriangle;        // 上三角形路径
    private Path downTriangle;      // 下三角形路径
    private Bitmap stickBitmap;     // 摇杆图片

    private int stickImageResId = R.drawable.arr_fb; // 默认图片资源ID

    // 回调接口
    public interface OnJoystickMoveListener {
        void onValueChanged(float xPercent, float yPercent);
        void onReleased();
    }
    private OnJoystickMoveListener listener;

    // 构造方法


    public JoystickView(Context context) {
        super(context);
        init(null);
    }

    public JoystickView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

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

    private void init(AttributeSet attrs) {

// 从XML属性获取自定义属性
        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.JoystickView);
            stickImageResId = a.getResourceId(R.styleable.JoystickView_stickImage, R.drawable.arr_fb);
            a.recycle();
        }

        // 初始化底座画笔(主灰色)
        basePaint = new Paint();
        //basePaint.setColor(Color.rgb(150, 150, 150));
        basePaint.setColor(Color.rgb(47, 47, 47));
        basePaint.setStyle(Paint.Style.FILL);
        basePaint.setAntiAlias(true);

        // 初始化外圈浅灰色圆环(宽度2)
        outerRingPaint = new Paint();
        outerRingPaint.setColor(Color.rgb(200, 200, 200));
        outerRingPaint.setStyle(Paint.Style.STROKE);
        outerRingPaint.setStrokeWidth(6f);
        outerRingPaint.setAntiAlias(true);

        // 初始化中间白色圆环(宽度3)
        middleRingPaint = new Paint();
        middleRingPaint.setColor(Color.WHITE);
        middleRingPaint.setStyle(Paint.Style.STROKE);
        middleRingPaint.setStrokeWidth(9f);
        middleRingPaint.setAntiAlias(true);


        // 初始化三角形画笔(白色)
        trianglePaint = new Paint();
        trianglePaint.setColor(Color.WHITE);
        trianglePaint.setStyle(Paint.Style.FILL);
        trianglePaint.setAntiAlias(true);


        //stickBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.arr_fb);
        if (stickBitmap != null && !stickBitmap.isRecycled()) {
            stickBitmap.recycle();
        }
        stickBitmap = BitmapFactory.decodeResource(getResources(), stickImageResId);

        centerPoint = new PointF();
        stickPoint = new PointF();
        upTriangle = new Path();
        downTriangle = new Path();


    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // 计算中心点
        centerPoint.set(w / 2f, h / 2f);
        stickPoint.set(centerPoint);

        // 计算半径
        baseRadius = Math.min(w, h) * 0.4f;
        stickRadius = baseRadius * 0.3f;
        maxDistance = baseRadius - stickRadius - 20;

        // 如果图片太大,可以缩放
        if (stickBitmap != null) {
            int desiredSize = (int)(stickRadius * 2);
            stickBitmap = Bitmap.createScaledBitmap(stickBitmap, desiredSize, desiredSize, true);
        }

        // 初始化三角形路径
        updateTrianglePaths();
    }

    private void updateTrianglePaths() {
        /*
        float triangleHeight = stickRadius * 0.4f; // 三角形高度
        float triangleBase = (float) (2 * triangleHeight / Math.tan(Math.toRadians(30))); // 计算底边长度(120度角)
        float triangleSpacing = stickRadius * 0.4f; // 三角形间距

        // 上三角形(向上120度)
        upTriangle.reset();
        upTriangle.moveTo(stickPoint.x, stickPoint.y - triangleSpacing - triangleHeight);
        upTriangle.lineTo(stickPoint.x - triangleBase/2, stickPoint.y - triangleSpacing);
        upTriangle.lineTo(stickPoint.x + triangleBase/2, stickPoint.y - triangleSpacing);
        upTriangle.close();

        // 下三角形(向下120度)
        downTriangle.reset();
        downTriangle.moveTo(stickPoint.x, stickPoint.y + triangleSpacing + triangleHeight);
        downTriangle.lineTo(stickPoint.x - triangleBase/2, stickPoint.y + triangleSpacing);
        downTriangle.lineTo(stickPoint.x + triangleBase/2, stickPoint.y + triangleSpacing);
        downTriangle.close();
        */

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 1. 绘制底座主圆
        canvas.drawCircle(centerPoint.x, centerPoint.y, baseRadius, basePaint);

        // 2. 绘制外圈浅灰色圆环(最外层)
        canvas.drawCircle(centerPoint.x, centerPoint.y, baseRadius - 3f, outerRingPaint);

        // 3. 绘制中间白色圆环
        canvas.drawCircle(centerPoint.x, centerPoint.y, baseRadius - 9f, middleRingPaint);

        // 4. 绘制摇杆图片(替换原来的圆圈)
        if (stickBitmap != null) {
            canvas.drawBitmap(stickBitmap,
                    stickPoint.x - stickBitmap.getWidth()/2,
                    stickPoint.y - stickBitmap.getHeight()/2,
                    null);
        } else {
            // 如果图片加载失败,绘制默认圆圈
            Paint stickPaint = new Paint();
            stickPaint.setColor(Color.rgb(220, 220, 220));
            stickPaint.setStyle(Paint.Style.FILL);
            stickPaint.setAntiAlias(true);
            canvas.drawCircle(stickPoint.x, stickPoint.y, stickRadius, stickPaint);
        }

        // 5. 绘制三角形(白色)
        canvas.drawPath(upTriangle, trianglePaint);
        canvas.drawPath(downTriangle, trianglePaint);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        // 回收Bitmap资源
        if (stickBitmap != null && !stickBitmap.isRecycled()) {
            stickBitmap.recycle();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float touchX = event.getX();
        float touchY = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                float dx = touchX - centerPoint.x;
                float dy = touchY - centerPoint.y;
                float distance = (float) Math.sqrt(dx * dx + dy * dy);

                if (distance <= maxDistance) {
                    stickPoint.set(touchX, touchY);
                } else {
                    float ratio = maxDistance / distance;
                    stickPoint.set(
                            centerPoint.x + dx * ratio,
                            centerPoint.y + dy * ratio
                    );
                }

                updateTrianglePaths();

                float xPercent = (stickPoint.x - centerPoint.x) / maxDistance;
                float yPercent = (stickPoint.y - centerPoint.y) / maxDistance;

                if (listener != null) {
                    listener.onValueChanged(xPercent, -yPercent);
                }

                invalidate();
                return true;

            case MotionEvent.ACTION_UP:
                stickPoint.set(centerPoint);
                updateTrianglePaths();
                invalidate();

                if (listener != null) {
                    listener.onReleased();
                }
                return true;
        }
        return super.onTouchEvent(event);
    }

    public void setOnJoystickMoveListener(OnJoystickMoveListener listener) {
        this.listener = listener;
    }
}

2.2  添加属性文件res\values\attrs.xml文件,当外部调用希望通过界面文件在初始化就传入参数的时候,需要添加属性文件。

<resources>
    <declare-styleable name="JoystickView">
        <attr name="stickImage" format="reference" />
    </declare-styleable>
</resources>

2.3  activity_first.xml文件代码,通过设置新建属性可以设置摇杆中心的图片,app:stickImage="@drawable/arr_lr"

<?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="match_parent"

    android:orientation="vertical"

    android:background="#FF2F2F2F"
    >



    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:text="" />
        <com.hy.ble.send.JoystickView
            android:id="@+id/joystickLeft"
            android:layout_width="220dp"
            android:layout_height="200dp"
            app:stickImage="@drawable/arr_fb"
            android:layout_centerInParent="true"

            />

        <TextView

            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:text="" />



        <com.hy.ble.send.JoystickView
            android:id="@+id/joystickRight"
            android:layout_width="200dp"
            android:layout_height="200dp"
            app:stickImage="@drawable/arr_lr"
            android:layout_centerInParent="true"

            />
        <TextView

            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:text="" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:background="@drawable/bottom"
        ></LinearLayout>
</LinearLayout>

2.3  mainactivity.java的调用

package com.hy.ble.send;


import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelUuid;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.tbruyelle.rxpermissions2.RxPermissions;
import com.trello.rxlifecycle2.android.ActivityEvent;
import com.trello.rxlifecycle2.components.support.RxAppCompatActivity;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;


import butterknife.ButterKnife;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.Nullable;

public class FirstActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

// 在Activity中初始化两个JoystickView
        JoystickView joystickLeft = findViewById(R.id.joystickLeft);


        JoystickView joystickRight = findViewById(R.id.joystickRight);


// 为左摇杆设置监听
        joystickLeft.setOnJoystickMoveListener(new JoystickView.OnJoystickMoveListener() {
            @Override
            public void onValueChanged(float xPercent, float yPercent) {
                // 这里可以添加控制逻辑,例如:
                // - 控制机器人移动
                // - 控制游戏角色
                // - 控制无人机等

                // 示例:根据摇杆位置控制电机
                if (xPercent > 0.5f) {

                   //向右
                } else if (xPercent < -0.5f) {
                    // 向左转
                }

                if (yPercent > 0.5f) {
                    //前进

                } else if (yPercent < -0.5f) {
                    // 后退

                }

                Log.d("Joystick", "Left - X: " + xPercent + ", Y: " + yPercent);

            }

            @Override
            public void onReleased() {
                // 处理左摇杆释放

                Log.d("JoystickLeft", "Left released");
            }
        });

// 为右摇杆设置监听
        joystickRight.setOnJoystickMoveListener(new JoystickView.OnJoystickMoveListener() {
            @Override
            public void onValueChanged(float xPercent, float yPercent) {
                // 这里可以添加控制逻辑,例如:
                // - 控制机器人移动
                // - 控制游戏角色
                // - 控制无人机等

                // 示例:根据摇杆位置控制电机
                if (xPercent > 0.5f) {
                    // 向右转

                } else if (xPercent < -0.5f) {
                    // 向左转

                }

                if (yPercent > 0.5f) {
                    // 前进

                } else if (yPercent < -0.5f) {
                    // 后退

                }

                Log.d("Joystick", "right - X: " + xPercent + ", Y: " + yPercent);

            }

            @Override
            public void onReleased() {
                // 处理左摇杆释放

                Log.d("JoystickRight", "right released");
            }
        });
       
    }
}

3.效果图


网站公告

今日签到

点亮在社区的每一天
去签到