`
isiqi
  • 浏览: 16491476 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

Android 自定义控件-SnakeLayout (仿gallery)

阅读更多

转载请注明转载地址:

http://wallage.blog.163.com/blog/static/1738962420108211120850/

简要介绍:相信大部分用过android Gallery控件的人,对gallery这个控件可谓是又爱又恨,gallery动画效果不错,非常实用,可是却有很多限制,从布局上来讲,gallery仅能水平放置,若想使用垂直放置的gallery,除非重写gallery。本文所述SnakeLayout继承于FrameLayout,用户可在SnakeLayout里自定义多个ImageView (大于等于3)的位置,并将指定的ID分配给所定义的ImageView;之后在主文件里进行简单的初始化后,就可以像gallery一样拖动所定义的ImageView,如同一条蛇一样连续的移动,不仅能横着拖,竖着拖,还能斜着拖,甚至绕圈圈。

效果图如下(android虚拟机长宽为800*600):

Android 自定义控件-SnakeLayout (仿gallery) - Wallace - 懒羊羊的南瓜屋
Android 自定义控件-SnakeLayout (仿gallery) - Wallace - 懒羊羊的南瓜屋
(测试版)代码如下:
package com.Snake;
/*
* Author: Wallace Wang
* Email: wallage@qq.com
*/
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.GestureDetector.OnGestureListener;
import android.widget.FrameLayout;
import android.widget.ImageView;
public class SnakeLayout extends FrameLayout {
private static final String LOG_TAG = "SnakeLayout";
private GestureDetector mGestureDetector;
private SnakeOnGestureListener mGestureListener;
private List<View> ViewHolder;
private int selectImg;
private int totalViewNum;
private View mContentView;
private SnakeView ScrollView;
private Context mContext;
private enum State {
ABOUT_TO_ANIMATE,
ANIMATING,
ANIMATE_END,
READY,
TRACKING
};
private State mState;
private double aniStartPos;// Value = scrollNum + percent*direction;
private double aniStopPos;// Value = scrollNum + percent*direction;
private Date aniStartTime;
private long aniTime = 1000;
private double aniSpeed = 500;
private double aniDefG = 5;

private int mContentWidth = 0;
private int mContentHeight = 0;
private int clickItem = -1;
private int direction = 0;
private int movDirection = 0;
private double percent = 0;
private int scrollNum = 0;
private PathScale myPathViews;
private List<Bitmap> BmpRecViews;
private OnSelectListener selectListener;
private OnClickListener clickListener;
private int currentIndex = 0;
public SnakeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
Log.d(LOG_TAG, "Init Snake Layout");
mContext = context;
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.SnakeLayout);
selectImg = a.getInteger(R.styleable.SnakeLayout_selectImg, -1);
a.recycle();
mGestureListener = new SnakeOnGestureListener();
mGestureDetector = new GestureDetector(mGestureListener);
mGestureDetector.setIsLongpressEnabled(false);
BmpRecViews = new ArrayList<Bitmap>();
myPathViews = new PathScale();
mState = State.READY;
}
public void Init(){
for (int i = 0; i < totalViewNum; i++) {
ImageView v = (ImageView)ViewHolder.get(i);
v.setScaleType(ImageView.ScaleType.FIT_XY);
v.setImageBitmap(BmpRecViews.get((i + currentIndex)% BmpRecViews.size()));
}
}

public void addBitmap(Bitmap b){
if(b != null)
BmpRecViews.add(b);
}

public void addBitmap(Bitmap b, int position){
if(b != null)
BmpRecViews.add(position, b);
}

public void addRec(int rec){
Bitmap b = BitmapFactory.decodeResource(this.getResources(),rec);
if(b != null)
BmpRecViews.add(b);
}
......
代码太长,省略
res/values/attrs.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SnakeLayout">
<!-- Defines the special selected position Image -->
<attr name="selectImg" format="integer" />
</declare-styleable>
</resources>
res/values/ids.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="snakeImg0" />
<item type="id" name="snakeImg1" />
<item type="id" name="snakeImg2" />
<item type="id" name="snakeImg3" />
<item type="id" name="snakeImg4" />
<item type="id" name="snakeImg5" />
<item type="id" name="snakeImg6" />
<item type="id" name="snakeImg7" />
<item type="id" name="snakeImg8" />
<item type="id" name="snakeImg9" />
<item type="id" name="snakeImg10" />
<item type="id" name="snakeImg11" />
<item type="id" name="snakeImg12" />
<item type="id" name="snakeImg13" />
<item type="id" name="snakeImg14" />
<item type="id" name="snakeImg15" />
<item type="id" name="snakeImg16" />
<item type="id" name="snakeImg17" />
<item type="id" name="snakeImg18" />
<item type="id" name="snakeImg19" />
<item type="id" name="snakeImg20" />
<item type="id" name="snakeImg21" />
<item type="id" name="snakeImg22" />
<item type="id" name="snakeImg23" />
<item type="id" name="snakeImg24" />
<item type="id" name="snakeImg25" />
<item type="id" name="snakeImg26" />
<item type="id" name="snakeImg27" />
<item type="id" name="snakeImg28" />
<item type="id" name="snakeImg29" />
<item type="id" name="snakeContent" />
</resources>
主布局文件:main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:snake="http://schemas.android.com/apk/res/com.Snake"
android:orientation="vertical" android:background="@drawable/bj"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.Snake.SnakeLayout android:layout_width="fill_parent" android:layout_weight="1"
android:id="@+id/my_snake" android:layout_height="fill_parent"
snake:selectImg="7">
<LinearLayout android:layout_width="fill_parent" android:orientation="vertical"
android:layout_height="wrap_content" android:id="@id/snakeContent">
<LinearLayout android:layout_width="fill_parent" android:paddingTop="70dip"
android:layout_height="wrap_content">
<TextView android:layout_width="270dip" android:layout_height="1dip"/>
<ImageView android:layout_width="40dip" android:id="@id/snakeImg0"
android:layout_height="40dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="300dip" android:layout_height="1dip"/>
<ImageView android:layout_width="60dip" android:id="@id/snakeImg1"
android:layout_height="60dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="295dip" android:layout_height="1dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg2"
android:layout_height="50dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="290dip" android:layout_height="1dip"/>
<ImageView android:layout_width="35dip" android:id="@id/snakeImg3"
android:layout_height="35dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="290dip" android:layout_height="1dip"/>
<ImageView android:layout_width="35dip" android:id="@id/snakeImg4"
android:layout_height="35dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="292dip" android:layout_height="1dip"/>
<ImageView android:layout_width="40dip" android:id="@id/snakeImg5"
android:layout_height="40dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="320dip" android:layout_height="1dip"/>
<ImageView android:layout_width="40dip" android:id="@id/snakeImg6"
android:layout_height="40dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg7"
android:layout_height="55dip" android:paddingTop="15dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg8"
android:layout_height="55dip" android:paddingTop="15dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg9"
android:layout_height="55dip" android:paddingTop="15dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg10"
android:layout_height="50dip" android:paddingTop="10dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg11"
android:layout_height="40dip" android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg12"
android:layout_height="40dip" android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg13"
android:layout_height="50dip" android:paddingLeft="10dip"
android:paddingTop="10dip"/>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:layout_width="260dip" android:layout_height="1dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg21"
android:layout_height="25dip"
android:paddingLeft="25dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg20"
android:layout_height="55dip" android:paddingTop="20dip"
android:paddingLeft="15dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg19"
android:layout_height="65dip" android:paddingTop="25dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg18"
android:layout_height="65dip" android:paddingTop="25dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg17"
android:layout_height="55dip" android:paddingTop="15dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="50dip" android:id="@id/snakeImg16"
android:layout_height="45dip" android:paddingTop="5dip"
android:paddingLeft="10dip"/>
<ImageView android:layout_width="45dip" android:id="@id/snakeImg15"
android:layout_height="35dip" android:paddingLeft="10dip"/>
<ImageView android:layout_width="45dip" android:id="@id/snakeImg14"
android:layout_height="35dip" android:paddingLeft="10dip"/>
</LinearLayout>
</LinearLayout>
</com.Snake.SnakeLayout>
</LinearLayout>
Snake.java 文件:
package com.Snake;
import android.app.Activity;
import android.os.Bundle;
public class Snake extends Activity {
/** Called when the activity is first created. */
SnakeLayout mSnake;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

mSnake = (SnakeLayout)findViewById(R.id.my_snake);
mSnake.addRec(R.drawable.png1);
mSnake.addRec(R.drawable.png2);
mSnake.addRec(R.drawable.png3);
mSnake.addRec(R.drawable.png4);
mSnake.addRec(R.drawable.png5);
mSnake.addRec(R.drawable.png6);
mSnake.addRec(R.drawable.png7);
mSnake.addRec(R.drawable.png8);
mSnake.addRec(R.drawable.png9);
mSnake.addRec(R.drawable.png10);
mSnake.addRec(R.drawable.png11);
mSnake.addRec(R.drawable.png12);
mSnake.addRec(R.drawable.png13);
mSnake.addRec(R.drawable.png14);
mSnake.addRec(R.drawable.png15);
mSnake.addRec(R.drawable.png16);
mSnake.addRec(R.drawable.png17);
mSnake.addRec(R.drawable.png18);
mSnake.addRec(R.drawable.png19);
mSnake.addRec(R.drawable.png20);
mSnake.addRec(R.drawable.png21);
mSnake.addRec(R.drawable.png22);
mSnake.addRec(R.drawable.png23);
mSnake.addRec(R.drawable.png24);
mSnake.addRec(R.drawable.png25);
mSnake.addRec(R.drawable.png26);
mSnake.addRec(R.drawable.png27);
mSnake.addRec(R.drawable.png28);
mSnake.Init();
}
}
分享到:
评论

相关推荐

    自定义组件:SnakeLayout

    【标题】:“自定义组件:SnakeLayout” 在Android开发中,自定义组件是...通过深入学习SnakeLayout,开发者不仅可以掌握一个实用的工具,还能提升自身在Android自定义组件开发上的能力,为未来的项目积累宝贵经验。

    用SnakeLayout实现纵向Gallery完整代码

    SnakeLayout是一种自定义布局,它在Android开发中用于创建各种动态翻页效果,如这里的纵向Gallery。这个完整的代码示例提供了如何使用SnakeLayout来实现一个可以从上至下或从下至上的翻页效果,类似于画廊中的图片...

    FlexLayouts 布局

    Flex 自定义布局 FlowLayout SnakeLayout CircleLayout RectangleLayout 官方下载地址:http://flexlayouts.org/download/

    自定义布局 FlexLayouts 源码

    FlexLayouts 自定义布局源码 src\org\flexlayouts\layouts\CircleLayout.as src\org\flexlayouts\layouts\FlowLayout.as src\org\flexlayouts\layouts\RectangleLayout.as src\org\flexlayouts\layouts\SnakeLayout....

    基于python与Django的网上购物平台

    基于python与Django的网上购物平台,页面整洁美观,主要功能有: 1、首页包括我的订单、购物车、我的收藏、我的足迹 2、商品分类查找、商品搜索、待收货、待发货、代付款 3、商品详情信息、配送地址选择、加入购物车 4、系统的登录和注册 使用的是mysql数据库,适合初学者下载使用。

    数据库设计管理课程设计系统设计报告(powerdesign+sql+DreamweaverCS)超市管理系统设计与开发2

    数据库设计管理课程设计系统设计报告(powerdesign+sql+DreamweaverCS)超市管理系统设计与开发2提取方式是百度网盘分享地址

    基于springboot的物流管理系统源码数据库文档.zip

    基于springboot的物流管理系统源码数据库文档.zip

    springboot285基于Java web的药店管理系统的设计与实现.zip

    论文描述:该论文研究了某一特定领域的问题,并提出了新的解决方案。论文首先对问题进行了详细的分析和理解,并对已有的研究成果进行了综述。然后,论文提出了一种全新的解决方案,包括算法、模型或方法。在整个研究过程中,论文使用了合适的实验设计和数据集,并进行了充分的实验验证。最后,论文对解决方案的性能进行了全面的评估和分析,并提出了进一步的研究方向。 源码内容描述:该源码实现了论文中提出的新的解决方案。源码中包含了算法、模型或方法的具体实现代码,以及相关的数据预处理、实验设计和性能评估代码。源码中还包括了合适的注释和文档,以方便其他研究者理解和使用。源码的实现应该具有可读性、可维护性和高效性,并能够复现论文中的实验结果。此外,源码还应该尽可能具有通用性,以便在其他类似问题上进行进一步的应用和扩展。

    基于springboot云平台的信息安全攻防实训平台源码数据库文档.zip

    基于springboot云平台的信息安全攻防实训平台源码数据库文档.zip

    2010-2022年地区社会信任水平(CGSS调查数据)-最新出炉.zip

    2010-2022年地区社会信任水平(CGSS调查数据)-最新出炉 2010-2022年cgss社会信任,原始数据及处理代码!! 包括: trust1上市公司所在省份的社会信任水平,等于CGSS中33的回复中“非常同意”和“比较同意”的人数占该省回复人数总数的比重; trust2上市公司所在省份的社会信任水平,对于CGSS中a33问题回复“非常不同意”“比较不同意”“说不上同意不同意”“比较同意”“非常同意”的,分别赋值为-2、-1、0、1、2,然后,取该省份所有回复的平均值。

    (源码)基于MCU和C语言的数字时钟系统.zip

    # 基于MCU和C语言的数字时钟系统 ## 项目简介 这是一个数字时钟系统的设计与实现项目,结合了电路设计与嵌入式编程技术。本项目包含了电路设计、PCB板设计和基于微控制器(MSP4302553)的C语言程序开发。数字时钟功能包括时间显示、闹钟提醒等。 ## 项目的主要特性和功能 1. 基于微控制器MSP4302553实现数字时钟功能。 2. 支持时间显示,包括小时、分钟和秒。 3. 支持闹钟提醒功能。 4. PCB板设计,方便硬件制作和集成。 ## 安装使用步骤 假设用户已经下载了本项目的源码文件和相关硬件设计文件。 1. 安装并熟悉MSP430微控制器的编程环境,如Energia IDE。 2. 根据提供的PCB设计文件制作硬件电路,确保电路连接正确无误。 3. 将编译好的C语言程序烧录到MSP430微控制器中。 4. 完成硬件电路的组装和调试。确保数字时钟正常工作,显示时间准确。

    基于springboot的城市公交查询系统源码数据库文档.zip

    基于springboot的城市公交查询系统源码数据库文档.zip

    (源码)基于JavaEE和Layui的技术论坛系统.zip

    # 基于JavaEE和Layui的技术论坛系统 ## 项目简介 这是一个基于JavaEE和Layui框架开发的技术论坛系统,旨在为技术爱好者提供一个交流和讨论的平台。系统支持用户注册、登录、发帖、回复、管理论坛板块等功能,适用于小型技术社区的搭建。 ## 项目的主要特性和功能 1. 用户管理 用户注册、登录、修改个人信息。 管理员权限管理,包括删除用户。 2. 论坛管理 添加、删除、修改论坛板块。 查看所有论坛板块及其详细信息。 3. 帖子管理 发布、删除、查看帖子。 根据论坛板块分类查看帖子。 4. 回复管理 发布、删除回复。 查看指定帖子的所有回复。 ## 安装使用步骤 ### 环境准备 1. JDK确保已安装JDK 1.8或更高版本。 2. Tomcat下载并安装Apache Tomcat 9.0.24或更高版本。

    springboot303针对老年人的景区订票系统.zip

    论文描述:该论文研究了某一特定领域的问题,并提出了新的解决方案。论文首先对问题进行了详细的分析和理解,并对已有的研究成果进行了综述。然后,论文提出了一种全新的解决方案,包括算法、模型或方法。在整个研究过程中,论文使用了合适的实验设计和数据集,并进行了充分的实验验证。最后,论文对解决方案的性能进行了全面的评估和分析,并提出了进一步的研究方向。 源码内容描述:该源码实现了论文中提出的新的解决方案。源码中包含了算法、模型或方法的具体实现代码,以及相关的数据预处理、实验设计和性能评估代码。源码中还包括了合适的注释和文档,以方便其他研究者理解和使用。源码的实现应该具有可读性、可维护性和高效性,并能够复现论文中的实验结果。此外,源码还应该尽可能具有通用性,以便在其他类似问题上进行进一步的应用和扩展。

    基于python+MySQL实现高校学籍管理系统功能齐全,使用了hash函数单向加密等密码学技术课程设计(源码+课设报告)

    【作品名称】:基于python+MySQL实现的,针对老师、学生、管理员用户,功能齐全,使用了hash函数单向加密等密码学技术。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【项目介绍】: 功能要求 实现学生信息、班级、院系、专业等的管理; 实现课程、学生成绩信息管理; 实现学生的奖惩信息管理; 创建规则用于限制性别项只能输入"男"或"女"; 创建视图查询各个学生的学号、姓名、班级、专业、院系; 创建存储过程查询指定学生的成绩单; 创建触发器当增加、删除学生和修改学生班级信息时自动修改相应班级学生人数; 建立数据库相关表之间的参照完整性约束。 学籍管理系统的功能需求包括管理员、学生和教师对功能的需求的三大部分: 1. 管理员对功能的需求: 管理员权限最大,可以对学生、教师、课程进行管理,包括对学生学籍信息的增删改查,对教 【资源声明】:本资源作为“参考资料”而不是“定制需求”,代码只能作为参考,不能完全复制照搬。需要有一定的基础看懂代码,自行调试代码并解决报错,能自行添加功能修改代码。

    【多式联运】基于模糊需求和模糊运输时间的多式联运路径优化附Matlab代码.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    (源码)基于Spring Boot和Vue的物业管理系统.zip

    # 基于Spring Boot和Vue的物业管理系统 ## 项目简介 本项目是一个基于Spring Boot和Vue的物业管理系统,旨在提供一个高效、易用的平台,帮助物业公司管理小区、楼栋、房产、业主、车辆等信息。系统支持用户管理、权限控制、数据统计等功能,适用于各类物业管理场景。 ## 项目的主要特性和功能 ### 用户管理 用户登录与权限控制系统支持多用户登录,并根据用户角色进行权限控制,确保不同用户只能访问其权限范围内的功能和数据。 用户信息管理管理员可以查看、编辑和删除用户信息,包括用户的基本信息、角色和权限。 ### 小区管理 小区信息管理管理员可以添加、编辑和删除小区信息,包括小区名称、地址、面积、总栋数、总户数等。 小区状态管理管理员可以设置小区的状态(正常或停用),并查看小区的详细信息。 ### 楼栋管理 楼栋信息管理管理员可以添加、编辑和删除楼栋信息,包括楼栋名称、所属小区、总户数等。

    基于springboot+Vue框架的学生交流互助平台源码数据库文档.zip

    基于springboot+Vue框架的学生交流互助平台源码数据库文档.zip

    Nvidia GeForce GT 1010驱动(适用Win7、Win8)

    NVIDIA GeForce GT 1010 是英伟达推出的一款入门级桌面显卡,以下是它的详细介绍: 基本信息 发布时间:2021 年 1 月 13 日. 核心代号:GP108. 制造工艺:14 纳米. 性能参数 核心频率:基础频率为 1228MHz,加速频率可达 1468MHz. 显存类型:GDDR5 ,显存容量为 2GB,位宽 64bit,显存频率为 6GHz ,显存带宽 48.06GB/s. 流处理器数量:256 个. 纹理单元:16 个. 光栅单元:8 个. 功耗与供电 功耗:热设计功耗(TDP)仅为 30W,无需额外的电源连接器,可直接通过 PCI-E 插槽供电. 建议电源:200W 及以上. 显示输出接口 配备 1 个 DVI 接口、1 个 Mini-HDMI 2.0 接口,可满足基本的显示输出需求. 性能表现 游戏性能:由于其规格较低,它主要适用于轻度游戏,如独立游戏、像素风格游戏、模拟经营及怀旧老款游戏等。在面对大型 3D 游戏或对画质和帧率要求较高的游戏时,可能无法提供流畅的游戏体验. 日常应用:能够满足日常的办公需求,如文档处理、网页浏览等,也可以流畅播放高清视频.

    (源码)基于Arduino的RS232通讯拦截系统.zip

    # 基于Arduino的RS232通讯拦截系统 ## 项目简介 本项目旨在拦截并观察RS232设备之间的ASCII通信。通过在两个RS232设备之间插入Arduino,用户可以调试或逆向工程设备的命令集或协议。项目基于Arduino平台开发,适用于需要深入探究RS232通信的用户。 ## 项目的主要特性和功能 1. 通信拦截拦截并打印两个RS232设备之间的通信内容。 2. 调试与分析通过Arduino的串口监视器查看通信内容,方便调试和协议分析。 3. 硬件指导提供详细的硬件连接示意图和软件操作流程指导。 ## 安装使用步骤 假设用户已经下载了本项目的源码文件。 1. 准备硬件 两个RS232toTTL转换器 Arduino(建议使用Mega 2560以获取足够的串口资源) 连接线等 2. 连接硬件

Global site tag (gtag.js) - Google Analytics