Android自定义view

news2024/11/15 7:18:25

前言

在Android开发时,我们经常会碰见在很多地方会重复使用相同的布局,或者是需要显示一些非基础组件,这个时候我们第一反应就是去自定义布局。将很多常用的UI业务需求,封装成一个View来操作,可以有效加快我们编码和开发效率。

自定义View的实现方式

实现方式常用的有两种:
1.通过继承Layout布局(相对来说简单易实现)
2.通过继承View(需要对onMeasure、onLayout和onDraw有一定的了解)

另外很重要的一点是自定义View和自定义属性的联合使用。

下面具体介绍两种方式的实现过程:

1. 继承Layout布局
  1. 首先你要写一个布局文件,里面包括了一个ImageView展示图片,一个TextView展示文字
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/top_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:layout_centerInParent="true"
        android:layout_alignParentTop="true"/>

    <TextView
        android:id="@+id/bottom_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_below="@+id/top_image"
        android:layout_marginTop="5dp"
        android:layout_centerInParent="true" />
</RelativeLayout>
  1. 写一个继承于对应Layout的自定义类
public class ImageTextLayoutView extends RelativeLayout {

	private ImageView topImage;
	private TextView bottomText;

	/**
	 * 这个构造方法是在代码中new的时候调用的
	 * @param context
	 */
	public ImageTextLayoutView(Context context) {
		super(context);
	}

	/**
	 * 这个构造方法是在xml文件中初始化调用的
	 * @param context
	 * @param attrs			View的xml属性
	 */

	public ImageTextLayoutView(Context context, @Nullable AttributeSet attrs) {
		super(context, attrs);
		initView(context,attrs);
	}

	/**
	 * 这个方法不常用,有前两个足够了
	 * @param context
	 * @param attrs
	 * @param defStyleAttr		应用到View的主题风格(定义在主题中)
	 */
	public ImageTextLayoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}


	private void initView(Context context,AttributeSet attrs){
		//获取子控件
		LayoutInflater.from(context).inflate(R.layout.image_text_view_layout,this);
		topImage = findViewById(R.id.top_image);
		bottomText = findViewById(R.id.bottom_text);

		//获取xml中定义的所有属性
		TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.ImageTextLayoutView);

		//获取ImageView相关自定义属性并设置
		int image_width = typedArray.getDimensionPixelSize(R.styleable.ImageTextLayoutView_image_width,30);
		int image_height = typedArray.getDimensionPixelSize(R.styleable.ImageTextLayoutView_image_height,30);
		Drawable drawable = typedArray.getDrawable(R.styleable.ImageTextLayoutView_image_src);
		//topImage.setLayoutParams(new LayoutParams(image_width,image_height));
		drawable.setBounds(0,0,image_width,image_height);
		topImage.setImageDrawable(drawable);

		//获取TextView相关的自定义属性并设置
		String content = typedArray.getString(R.styleable.ImageTextLayoutView_text_content);
		float text_size = typedArray.getDimension(R.styleable.ImageTextLayoutView_text_size,12);
		int text_color = typedArray.getColor(R.styleable.ImageTextLayoutView_text_color, Color.BLACK);
		bottomText.setText(content);
		bottomText.setTextSize(text_size);
		bottomText.setTextColor(text_color);
	}
}
  1. 最后在xml文件引用
<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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <com.ctsaing.flyandroid.ImageTextLayoutView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:image_height="40dp"
        app:image_src="@mipmap/ic_launcher"
        app:image_width="40dp"
        app:text_color="#aaff00"
        app:text_size="8sp"
        app:text_content="自定义View"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"/>
2. 通过继承View的方式
  1. 创建自定义 View 类。首先,你需要创建一个 Java 类来作为自定义 View 的基类。可以按照以下代码来创建一个基础的自定义 View 类:
public class CustomView extends View {
    public CustomView(Context context) {
        super(context);
    }

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

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}
  1. 重写 onMeasure() 方法。onMeasure() 方法用于测量 View 的大小,你可以在这个方法中设置 View 的宽度和高度等属性。以下是一个示例代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 获取 MeasureSpec 的模式和尺寸
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    // 根据模式设置自定义 View 的宽高
    int width, height;
    if (widthMode == MeasureSpec.EXACTLY) {
        // 如果宽度的测量模式是 EXACTLY,直接使用测量得到的尺寸
        width = widthSize;
    } else {
        // 否则根据自定义 View 的内容来计算宽度
        // 这里可以根据实际需要来设置宽度
        width = calculateWidth();
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        // 如果高度的测量模式是 EXACTLY,直接使用测量得到的尺寸
        height = heightSize;
    } else {
        // 否则根据自定义 View 的内容来计算高度
        // 这里可以根据实际需要来设置高度
        height = calculateHeight();
    }

    // 使用 setMeasuredDimension() 方法设置自定义 View 的宽高
    setMeasuredDimension(width, height);
}
  1. 重写 onLayout() 方法。onLayout() 方法用于确定子 View 在自定义 View 中的摆放位置。以下是一个示例代码:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // 遍历子 View,设置子 View 的位置
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        // 这里可以根据实际需要来设置子 View 的位置
        child.layout(left, top, right, bottom);
    }
}
  1. 重写 onDraw() 方法。onDraw() 方法用于绘制自定义 View 的内容。你可以在这个方法中使用 Canvas 对象来进行绘制操作。以下是一个示例代码:
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 这里可以使用 Canvas 对象来进行绘制操作
    // 可以绘制文字、图形、图片等
    
    // 例如,绘制一个红色的矩形
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
}

以上就是实现 Android 自定义 View 三个方法的基本步骤和代码示例。你可以根据实际需求来进行具体的实现和调整。

3. 自定义属性

自定义布局和自定义属性是很难分开的,自定义属性的好处是我们可以再xml文件中直接使用。下面是实现过程:

  1. 继承View
    针对上面的ImageTextLayoutView 布局,我们也可以直接通过自定义View来实现。不需要布局,直接继承View,然后重写onMeasure()和onDraw()方法。
package com.ctsaing.flyandroid.customViews;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.text.Layout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;


import com.ctsaing.flyandroid.R;

/**
 * 通过继承View实现上面图片下边文字的自定义View
 * created by cyh on 2019/12/25
 */
public class ImageTextCustomView extends View {

	private Drawable drawable;//画上方图片
	private String content;// 画下方文字

	//默认的View宽高
	private int viewWidth;
	private int viewHeight;


	//默认的属性值
	private int defalutImageWidth = 30;
	private int defalutImageHeight = 30;
	private int defalutTextColor = Color.BLACK;
	private float defalutTextSize = 8;

	//xml中获取到的属性值
	private int xmlImageWidth;
	private int xmlImageHeight;
	private int xmlTextColor;
	private float xmlTextSize;

	//计算text的长度
	private int textWidth;
	private int textHeight;

	//支持image和text的距离
	private int imageAndTextMargin;

	//画笔
	private Paint imagePaint = new Paint();
	private TextPaint textPaint = new TextPaint();

	public ImageTextCustomView(Context context) {
		super(context);
	}

	public ImageTextCustomView(Context context, @Nullable AttributeSet attrs) {
		super(context, attrs);
		initView(context, attrs, 0);
	}

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

	private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
		//先获取xml中的属性值
		TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ImageTextCustomView);
		xmlImageWidth = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageWidth, defalutImageWidth);
		xmlImageHeight = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageHeight, defalutImageHeight);
		drawable = typedArray.getDrawable(R.styleable.ImageTextCustomView_imageSrc);
		xmlTextColor = typedArray.getColor(R.styleable.ImageTextCustomView_textColor, defalutTextColor);
		xmlTextSize = typedArray.getDimension(R.styleable.ImageTextCustomView_textSize, defalutTextSize);
		content = typedArray.getString(R.styleable.ImageTextCustomView_textContent);
		imageAndTextMargin = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageAndTextMargin, 0);
		textHeight = (int) xmlTextSize;
		textPaint.setColor(xmlTextColor);
		textPaint.setTextSize(xmlTextSize);
		textWidth = (int) Layout.getDesiredWidth(content,textPaint);

		typedArray.recycle();
	}

	/**
	 * 重新测量View的宽高,并支持wrap_content设置
	 *
	 * @param widthMeasureSpec
	 * @param heightMeasureSpec
	 */

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);

		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);

		//获取到View的整个高度
		viewWidth = xmlImageWidth > textWidth ? xmlImageWidth : textWidth;
		viewHeight = xmlImageHeight + imageAndTextMargin + textHeight;
		viewWidth = Math.max(viewWidth, getSuggestedMinimumWidth());
		viewHeight = Math.max(viewHeight, getSuggestedMinimumHeight());

		/**
		 * 设置View的宽高
		 * tips:当xml文件中设置的是wrap_content时,Mode==AT_MOST
		 * 当设置位match_parent或具体的宽高时,Mode == EXACTLY
		 */
		if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
			setMeasuredDimension(viewWidth, viewHeight);
		} else if (widthMode == MeasureSpec.AT_MOST) {
			setMeasuredDimension(viewWidth, heightSize);
		} else if (heightMode == MeasureSpec.AT_MOST) {
			setMeasuredDimension(widthSize, viewHeight);
		} else {
			setMeasuredDimension(widthSize, heightSize);
		}


	}

	/**
	 * 在画布中间画出图片和文字,图片在上,文字在下
	 *
	 * @param canvas
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		//xml文件中image设置的宽高时,而实际要显示的图片宽高可能要大于设置值;因此要要对bitmap按比例缩放
		Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
		//获得bitmap的宽高
		int bitmapWidth = bitmap.getWidth();
		int bitmapHeight = bitmap.getHeight();
		//计算缩放比列
		float scaleWidth = ((float)xmlImageWidth)/bitmapWidth;
		float scaleHeight = ((float)xmlImageHeight)/bitmapHeight;
		//设置缩放的Matrix参数
		Matrix matrix = new Matrix();
		matrix.postScale(scaleWidth,scaleHeight);
		Bitmap newBitmap = Bitmap.createBitmap(bitmap,0,0,bitmapWidth,bitmapHeight,matrix,true);

		//开始画bitmap的位置
		float imageLeft = (viewWidth - xmlImageWidth) / 2;
		canvas.drawBitmap(newBitmap, imageLeft, 0, imagePaint);

		//画text的位置
		float y = xmlImageHeight + imageAndTextMargin;
		canvas.drawText(content,0,y,textPaint);
		bitmap.recycle();
		newBitmap.recycle();
		canvas.restore();
	}
}
  1. 在value文件夹中新建attrs.xml文件
    在这里插入图片描述
  2. 在attrs文件中声明我们想要的自定义属性
<declare-styleable name="ImageTextCustomView">
	<attr name="imageWidth" format="dimension"/>
	<attr name="imageHeight" format="dimension"/>
	<attr name="imageSrc" format="reference"/>
	<attr name="textSize" format="float|dimension"/>
	<attr name="textColor" format="color|reference"/>
	<attr name="textWidth" format="dimension"/>
	<attr name="textHeight" format="dimension"/>
	<attr name="textContent" format="string"/>
	<attr name="imageAndTextMargin" format="dimension"/>
</declare-styleable>
  1. 在我们的自定义文件中,通过typedArray取到我们在xml关于自定义View的属性值。
private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
	//先获取xml中的属性值
	TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ImageTextCustomView);
	xmlImageWidth = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageWidth, defalutImageWidth);
	xmlImageHeight = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageHeight, defalutImageHeight);
	drawable = typedArray.getDrawable(R.styleable.ImageTextCustomView_imageSrc);
	xmlTextColor = typedArray.getColor(R.styleable.ImageTextCustomView_textColor, defalutTextColor);
	xmlTextSize = typedArray.getDimension(R.styleable.ImageTextCustomView_textSize, defalutTextSize);
	content = typedArray.getString(R.styleable.ImageTextCustomView_textContent);
	imageAndTextMargin = typedArray.getDimensionPixelSize(R.styleable.ImageTextCustomView_imageAndTextMargin, 0);
	textHeight = (int) xmlTextSize;
	textPaint.setColor(xmlTextColor);
	textPaint.setTextSize(xmlTextSize);
	textWidth = (int) Layout.getDesiredWidth(content,textPaint);

	typedArray.recycle();
}
  1. 使用自定义属性的方式:
<?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.ctsaing.flyandroid.customViews.ImageTextCustomView
        android:layout_marginTop="50dp"
        android:layout_marginLeft="50dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:imageSrc="@mipmap/ic_launcher"
        app:imageWidth="40dp"
        app:imageHeight="40dp"
        app:imageAndTextMargin="10dp"
        app:textSize="10sp"
        app:textColor="@android:color/holo_red_dark"
        app:textContent="另一个自定义view"/>
</LinearLayout>

上面app: 后面的都是我们在attrs文件中声明的属性值,可以直接这样使用。

结语

本文讲述了关于自定义View的简单实现,主要是展示方面的,自定义view的内容还很多,之后再学习再交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2034709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

笔中藏音,非凡录制 — WT2003H录音芯片方案,让每一支笔都成为你的高清录音神器

开发背景&#xff1a; 在数字化时代&#xff0c;录音笔作为记录与传播声音的重要工具&#xff0c;其性能与功能的提升直接关系到用户的使用体验。随着科技的飞速发展&#xff0c;市场对于录音笔的需求不再仅仅局限于基本的录音功能&#xff0c;而是更加注重音质、便携性、耐用…

Java中包的使用

针对在同一个类中使用不同包中的同名类时&#xff0c;这时候我们需要使用全类名。

React学习-jsx语法

jsx语法&#xff0c;浏览器不认识&#xff0c;需要经过babel编译 https://babeljs.io/ 面试题&#xff1a;jsx的作用&#xff1f; 普通回答&#xff1a;可以在js中返回dom&#xff0c;经过babel编译成js认识的代码import { jsx as _jsx, jsxs as _jsxs } from "react/j…

Maven的理解与应用

Maven使用 一、Maven的含义 Maven是一个构建项目的工具&#xff0c;也是一个管理项目的工具 二、Maven的应用 构建项目 管理依赖 做项目的热部署 基于项目做多模块&#xff08;modle&#xff09;的构建 三、Maven的安装 注意&#xff1a;maven本身不需要安装&#xff0c;下…

JVM(十)深入理解JVM类加载模型以及双亲委派模型

本文详细介绍了Java的类加载机制&#xff0c;包括加载、链接和初始化三个阶段。强调了双亲委派模型的重要性&#xff0c;这是一种避免重复加载类并维持Java核心库安全的类加载策略。文章还探讨了Java类加载器的不同类型&#xff0c;包括启动类加载器、扩展类加载器和应用类加载…

Spring Cloud Alibaba 集成分布式定时任务调度功能

作者&#xff1a;千习 背景简介 定时任务是指在约定的时间&#xff0c;或者按照固定频率周期性执行的任务。在企业应用中&#xff0c;非用户行为发起的后台业务&#xff0c;一般都是通过定时任务来实现&#xff0c;常见场景如下&#xff1a; 异步数据处理&#xff1a;比如先…

【gpt生成文本的回复的原理和代码,通俗思路清晰】

首先介绍了贪婪解码 其次为增家多样性&#xff0c;用温度系数和TopK增加采样 真实的采样步骤 1、topk备选tokens 2、用维度系数大于1让概率平衡一下&#xff0c;3.再用softmax&#xff0c;4.根据概率分布采样 1、贪婪解码 # 之前&#xff0c;我们总是使用torch.argmax采样最大…

年薪30万+,TOP大厂月薪10万+....网络安全工程师凭什么?

时代飞速发展&#xff0c;我们的工作、生活乃至整个社会的运转都越来越依赖于网络。也因此&#xff0c;网络的无处不在带来了前所未有的安全风险。 从个人隐私泄露到企业机密被盗&#xff0c;再到国家关键基础设施遭受攻击&#xff0c;网络安全问题无处不在&#xff0c;威胁着…

SQL之使用存储过程循环插入数据

1、已经创建了任务日志表 CREATE TABLE t_task_log (id bigint NOT NULL AUTO_INCREMENT,task_id bigint NOT NULL COMMENT 任务ID,read_time bigint NOT NULL COMMENT 单位秒&#xff0c;读取耗时,write_time bigint NOT NULL COMMENT 单位秒&#xff0c;写入耗时,read_size …

8月13日学习笔记 LVS

一.描述以及工作原理 1. 什么是LVS linux virtural server的简称&#xff0c;也就是linxu虚拟机服务器&#xff0c;这是一个 由章文嵩博士发起的开源项目&#xff0c;官网是 http://www.linuxvirtualserver.org,现在lvs已经是linux内核标 准的一部分&#xff0c;使用lvs可以达…

网络剪枝——network-slimming 项目复现

目录 文章目录 目录网络剪枝——network-slimming 项目复现clone 存储库Baselinevgg训练结果 resnet训练结果 densenet训练结果 Sparsityvgg训练结果 resnet训练结果 densenet训练结果 Prunevgg命令结果 resnet命令结果 densenet命令结果 Fine-tunevgg训练结果 resnet训练结果 …

5个小众宝藏软件看看有没有你喜欢的

冷门APP分享来啦&#xff0c;这5个小众宝藏软件看看有没有你喜欢的吧&#xff01; 1.space登月计划 从地球到月球的大概距离是3.84亿米&#xff0c;而登月得消耗掉大约3.2亿千卡的能量。一个人想单飞登月得花上万年。 但在space上&#xff0c;可以和小伙伴一起合作玩登月游戏…

记录Java使用websocket

实现场景&#xff1a;每在小程序中添加一条数据时&#xff0c;后台将主动推送一个标记给PC端&#xff0c;PC端接收到标记将进行自动播放音频。 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import or…

GitHub 2FA中国认证教程

1. 问题描述 在github上有过代码贡献的账号在登录时需要进行2FA双重身份验证。 这是github官方给出的关于2FA的解释&#xff1a; 官方文章地址&#xff1a;点击进入 这是登录时2FA的验证界面&#xff1a; 我们需要使用扩展程序解析这个二维码拿到2FA验证码&#xff0c;填入二维…

python爬虫滑块验证及各种加密函数(基于ddddocr进行的一层封装)

git链接: https://github.com/JOUUUSKA/spider_toolsbox 这里写目录标题 一.识别验证码1、识别英文&#xff0b;数字验证码2、识别滑块验证码3、识别点选验证码 一.识别验证码 git链接: https://github.com/JOUUUSKA/spider_toolsbox 创作不易记得stars 1、识别英文&#xf…

Arduino控制带编码器的直流电机速度

Arduino DC Motor Speed Control with Encoder, Arduino DC Motor Encoder 作者 How to control dc motor with encoder:DC Motor with Encoder Arduino, Circuit Diagram:Driving the Motor with Encoder and Arduino:Control DC motor using Encoder feedback loop: How …

一文读懂Xinstall专属链接推广,轻松解决App运营痛点!

随着互联网的飞速发展&#xff0c;App推广和运营面临着前所未有的挑战。传统的营销方式已经难以适应多变的市场环境&#xff0c;而Xinstall专属链接推广应运而生&#xff0c;成为解决App获客难题的新利器。本文将深入探讨Xinstall专属链接推广如何帮助推广者触达更多用户&#…

MacOS vue-cli为2.9.6 无法升级的解决方案

背景 今天需要验证plop工具做前端工程化实践&#xff0c;打算使用vue3方式&#xff0c;结果发现vue-cli 2.9.6一直无法升级成功&#xff0c;也无法通过vue-cli生成vue3模板工程&#xff0c;测试了几把后&#xff0c;最终升级vue-cli成功&#xff0c;为了能给出现同样问题的小伙…

上瘾模型与产品激励系统

​产品要增加客户粘性&#xff0c;使产品深入人心就需要让用户对产品上瘾。如何使用户对产品上瘾&#xff1f;对于产品来说&#xff0c;就需要建立产品的激励系统。 产品的激励系统要做的事就是对用户进行激励&#xff0c;就是让用户主动完成产品或服务想要他们做的事情。 那么…

重启人生计划-勇敢者先行

&#x1f973;&#x1f973;&#x1f973; 茫茫人海千千万万&#xff0c;感谢这一刻你看到了我的文章&#xff0c;感谢观赏&#xff0c;大家好呀&#xff0c;我是最爱吃鱼罐头&#xff0c;大家可以叫鱼罐头呦~&#x1f973;&#x1f973;&#x1f973; 如果你觉得这个【重启人生…