Android 安全卫士 第二天_注意事项

Ctrl+T可以知道这个类的所有子类和父类。

如果你每个页面都有相同的布局,那么你就可以在 style文件里面定义一个相同的style。

然后这样就可以防止代码的重复使用。

  <style name="TitleStyle">
        <item name="android:gravity">center</item>
        <item name="android:textSize">20sp</item>
        <item name="android:textColor">#000</item>
        <item name="android:padding">10dp</item>
        <item name="android:background">#0f0</item>
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
    </style>

给你的控件设置<android:ellipsize="start middle end marquee none"属性,就是指定你的省略点的位置。

如果你设置的是marquee(跑马灯)就是一个滚动效果 如果你设置为none就是不设置点。

<!-- 想让文字出现跑马灯效果,必须让其获取焦点 -->

<!-- android:marqueeRepeatLimit="marquee_forever"一直滚动属性 -->

<!-- 自定义控件达到滚动效果(其实就是重写原有的TextView,让其一直能够获取焦点即可) -->

获取焦点:

android:focusable="true"

android:focusableInTouchMode="true"

————————

自定义控件:自动获取焦点

1.自定义控件编写流程

创建一个默认就能获取焦点的TextView

1.创建一个类继承至TextView,FocusTextView

2.重写其构造方法

public class FocusTextView extends TextView {
	//使用在通过java代码创建控件
	public FocusTextView(Context context) {
		super(context);
	}

	//由系统调用(带属性+上下文环境构造方法)
	public FocusTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	//由系统调用(带属性+上下文环境构造方法+布局文件中定义样式文件构造方法)
	public FocusTextView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	//重写获取焦点的方法,由系统调用,调用的时候默认就能获取焦点
	@Override
	public boolean isFocused() {//对应xml里面的 android:focusable="true"和android:focusableInTouchMode="true"
		return true;
	}
}

4.使用过程

获取当前类的全路径名称,作为xml中的标签存在,其余属性的使用方式和TextView一致

    <com.itheima.mobilesafe74.view.FocusTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="marquee"
        android:marqueeRepeatLimit="marquee_forever"
        android:padding="5dp"
        android:singleLine="true"
        android:text="秋天秋天悄悄过去,留下小秘密,啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊"
        android:textColor="#000" >
    </com.itheima.mobilesafe74.view.FocusTextView>

————————————————————————

2,GridView使用,和ListView使用方式类似

列数(3列)

数据填充(模块名称,模块图片)

数据适配器一致的

    <!--android:numColumns指定列数  -->
    <!-- android:verticalSpacing="10dp"指定内部条目竖直方向间距为10dp -->
    <GridView
        android:id="@+id/gv_home"
        android:numColumns="3"
        android:verticalSpacing="10dp"//竖直方向控件之间的间距。
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </GridView>

注意一点你设置适配器的时候,定义的布局文件中的控件一定要居中!!!

ImageView 有一个方法 setBackgroundResource(int id) 这个方法可以传递一个图片的id

进来 然后设置给这个控件.

传递的id为R.drawable.xxxxxx

————————————

当你需要设置一个控件,当做一条线的时候。直接使用<View>即可。

————————————————————————————

3,自定义组合控件

1.将已经编写好的布局文件,抽取到一个类中去做管理,下次还需要使用此布局结构的时候,

直接使用组合控件对应的对象.

2.将组合控件的布局,抽取到单独的一个xml中

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
    <RelativeLayout
        android:padding="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tv_title"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:id="@+id/tv_des"
            android:layout_below="@id/tv_title"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <CheckBox
            android:id="@+id/cb_box"

            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <View
            android:background="#000"
            android:layout_below="@id/tv_des"
            android:layout_width="match_parent"
            android:layout_height="1dp"/>
    </RelativeLayout>
</RelativeLayout>

3.通过一个单独的类,去加载此段布局文件.

这里注意一点,我们把一个view抽取出来,然后放到这个类里面,这个类要继承RelativeLayout。然后注意,我们要用这个类的构造方法,让他三个构造方法都调用最后一个构造方法。然后使用view.inflate()去实现将一个布局文件装换成一个view对象。

记住最后一个参数要写this,因为我们需要将前面抽取出来的控件作为一个root(根),让他加载到我们现在这个类里面。

public class SettingItemView extends RelativeLayout {public SettingItemView(Context context) {
		this(context,null);
	}

	public SettingItemView(Context context, AttributeSet attrs) {
		this(context, attrs,0);
	}

	public SettingItemView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		//xml--->view	将设置界面的一个条目转换成view对象,直接添加到了当前SettingItemView对应的view中
		View.inflate(context, R.layout.setting_item_view, this);

		//等同于以下两行代码
		/*View view = View.inflate(context, R.layout.setting_item_view, null);
		this.addView(view);*/

		//自定义组合控件中的标题描述
		TextView tv_title = (TextView) findViewById(R.id.tv_title);
		tv_des = (TextView) findViewById(R.id.tv_des);
		cb_box = (CheckBox) findViewById(R.id.cb_box);

	}

4.checkBox是否选中,决定SettingItemView是否开启,isCheck(){return checkbox.isCheck()}方法

5.提供一个SettingItemView,切换选中状态的方法setCheck(boolean isCheck)

在XML文件中设置

然后在SettingActivity中设置找到控件,并给他设置监听

4,设置界面,事件传递过程

SettingActivity对应布局文件的跟布局获取点击事件

此事件传递给SettingItemView

1.点击在SettingItemView非CheckBox区域,事件就由SettingItemView去做响应

2.点击在SettingItemView中CheckBox区域,事件就由SettingItemView传递给CheckBox,由CheckBox去做响应

CheckBox响应当前的点击事件,则SettingItemView就不能再去响应此事件,不能调用onClick方法,去改变状态

这个是第一种方式:

让我们的外部控件事件传递给内部控件,然后内部控件不去响应事件,在回传给外部控件。

第二种方式:

让我们的事件直接不能传递给内部控件,只能在外部控件。这个要去重写事件响应的API

这里我们使用第一种方式。

解决此问题的方案为:不让checkBox响应点击事件

<CheckBox
            android:id="@+id/cb_box"
            android:clickable="false"
            android:focusable="false"
            android:focusableInTouchMode="false"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

_____________________________________

sputil类:

用来保存一些配置。

比如保存你的checkBox的勾选情况,下次进来的时候可以自动使用上次的勾选。

public class SpUtil {
	private static SharedPreferences sp;
	/**
	 * 写入boolean变量至sp中
	 * @param ctx	上下文环境
	 * @param key	存储节点名称
	 * @param value	存储节点的值 boolean
	 */
	public static void putBoolean(Context ctx,String key,boolean value){
		//(存储节点文件名称,读写方式)
		if(sp == null){
			sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
		}
		sp.edit().putBoolean(key, value).commit();
	}
	/**
	 * 读取boolean标示从sp中
	 * @param ctx	上下文环境
	 * @param key	存储节点名称
	 * @param defValue	没有此节点默认值
	 * @return		默认值或者此节点读取到的结果
	 */
	public static boolean getBoolean(Context ctx,String key,boolean defValue){
		//(存储节点文件名称,读写方式)
		if(sp == null){
			sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
		}
		return sp.getBoolean(key, defValue);
	}

这里有一个问题就是。

如果你在其他类调用这个类的方法的时候。

		//获取已有的开关状态,用作显示
		boolean open_update = SpUtil.getBoolean(this, ConstantValue.OPEN_UPDATE, false);
		//是否选中,根据上一次存储的结果去做决定
		siv_update.setCheck(open_update);

这里我们在传递key的时候。因为key会复用所以我们直接用一个Constant类,来保存这些常量。

public class ConstantValue {
	/**
	 * 是否开启更新的key
	 */
	public static final String OPEN_UPDATE = "open_update";

	/**
	 * 是否设置密码key
	 */
	public static final String MOBILE_SAFE_PSD = "mobile_safe_psd";
}

————————————————————————————

注意一个问题:

private void initUpdate() {
		final SettingItemView siv_update = (SettingItemView) findViewById(R.id.siv_update);

		//获取已有的开关状态,用作显示
		boolean open_update = SpUtil.getBoolean(this, ConstantValue.OPEN_UPDATE, false);
		//是否选中,根据上一次存储的结果去做决定
		siv_update.setCheck(open_update);

		siv_update.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				//如果之前是选中的,点击过后,变成未选中
				//如果之前是未选中的,点击过后,变成选中

				//获取之前的选中状态
				boolean isCheck = siv_update.isCheck();
				//将原有状态取反,等同上诉的两部操作
				siv_update.setCheck(!isCheck);
				//将取反后的状态存储到相应sp中
				SpUtil.putBoolean(getApplicationContext(), ConstantValue.OPEN_UPDATE,!isCheck);

			}
		});

我们这里在onclicklistener()

里面去保存isCheck的状态的时候 调用SpUtil类的时候传递进去的context 不能是this

因为在

new OnClickListener()

这个内部类里面的this 是这个类的对象。

我们需要的context是activity里面的对象。所以我们需要用getApplicationContext();

_________________________________________

mHandler.sendMessageDelayed(msg, 4000);

//在发送消息4秒后去处理,ENTER_HOME状态码指向的消息

mHandler.sendEmptyMessageDelayed(ENTER_HOME, 4000);

这两个方法就是延时的去处理消息,不是延时去发送消息.

_______________________________________________________________

5,自定义属性,(设置中心,有多个条目,在复用SettingItemView的时候,每一个条目对应的标示,描述内容都不一致)

1.查看源码,定义属性时候做法

sdk所在目录\platforms\android-16\data\res\values\attrs.xml

<resource>

<declare-styleable name="TextView">

<attr name="text" format="string"/>

</declare-styleable>

</resource>

2.给SettingItemView定义属性,工程res\values\attrs.xml

<resource>

<declare-styleable name="SettingItemView全类名">

<attr name="destitle" format="string"/>

<attr name="desoff" format="string"/>

<attr name="deson" format="string"/>

</declare-styleable>

</resource>

3.自定义属性的使用

Android之所以能用控件的属性,比如<android:settext=""/>是因为我们定义了命名空间:xmlns:android="http://schemas.android.com/apk/res/android"。

这样就相当于连接到了这个文件的目录下定义好的属性。

所以我们要使用自定义的属性也需要给它命名空间。

定义名空间namespace

mobilesafe替换掉原有android

com.itheima.mobilesafe74必须这样编写,替换掉了android,代表当前应用自定义属性

xmlns:mobilesafe="http://schemas.android.com/apk/res/com.itheima.mobilesafe74"

<com.itheima.mobilesafe74.view.SettingItemView

xmlns:mobilesafe="http://schemas.android.com/apk/res/com.itheima.mobilesafe74"

android:id="@+id/siv_update"

android:layout_width="match_parent"

android:layout_height="wrap_content"

mobilesafe:destitle="自动更新设置"

mobilesafe:desoff="自动更新已关闭"

mobilesafe:deson="自动更新已开启">

4.获取属性值

mobilesafe:destitle="自动更新设置"

mobilesafe:desoff="自动更新已关闭"

mobilesafe:deson="自动更新已开启"

以上的是哪个属性都需要给自定义组合控件(SettingItemView)内部的两个TextView去使用,获取属性值

	//通过属性索引获取属性名称&属性值
	for(int i=0;i<attrs.getAttributeCount();i++){
		Log.i(tag, "name = "+attrs.getAttributeName(i));
		Log.i(tag, "value = "+attrs.getAttributeValue(i));
		Log.i(tag, "分割线 ================================= ");
	}
	//通过属性获取属性名称&名空间
	mDestitle = attrs.getAttributeValue(NAMESPACE, "destitle");
	mDesoff = attrs.getAttributeValue(NAMESPACE, "desoff");
	mDeson = attrs.getAttributeValue(NAMESPACE, "deson");

5.将获取的属性值,设置给title,des

des根据是否点中决定

true deson

false desoff

public class SettingItemView extends RelativeLayout {
	private static final String NAMESPACE = "http://schemas.android.com/apk/res/com.itheima.mobilesafe74";
	private static final String tag = "SettingItemView";
	private CheckBox cb_box;
	private TextView tv_des;
	private String mDestitle;
	private String mDesoff;
	private String mDeson;

	public SettingItemView(Context context) {
		this(context,null);
	}

	public SettingItemView(Context context, AttributeSet attrs) {
		this(context, attrs,0);
	}

	public SettingItemView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		//xml--->view	将设置界面的一个条目转换成view对象,直接添加到了当前SettingItemView对应的view中
		View.inflate(context, R.layout.setting_item_view, this);

		//等同于以下两行代码
		/*View view = View.inflate(context, R.layout.setting_item_view, null);
		this.addView(view);*/

		//自定义组合控件中的标题描述
		TextView tv_title = (TextView) findViewById(R.id.tv_title);
		tv_des = (TextView) findViewById(R.id.tv_des);
		cb_box = (CheckBox) findViewById(R.id.cb_box);

		//获取自定义以及原生属性的操作,写在此处,AttributeSet attrs对象中获取
		initAttrs(attrs);
		//获取布局文件中定义的字符串,赋值给自定义组合控件的标题
		tv_title.setText(mDestitle);
	}

	/**
	 * 返回属性集合中自定义属性属性值
	 * @param attrs	构造方法中维护好的属性集合
	 */
	private void initAttrs(AttributeSet attrs) {
		/*//获取属性的总个数
		Log.i(tag, "attrs.getAttributeCount() = "+attrs.getAttributeCount());
		//获取属性名称以及属性值
		for(int i=0;i<attrs.getAttributeCount();i++){
			Log.i(tag, "name = "+attrs.getAttributeName(i));
			Log.i(tag, "value = "+attrs.getAttributeValue(i));
			Log.i(tag, "分割线 ================================= ");
		}*/

		//通过名空间+属性名称获取属性值

		mDestitle = attrs.getAttributeValue(NAMESPACE, "destitle");
		mDesoff = attrs.getAttributeValue(NAMESPACE, "desoff");
		mDeson = attrs.getAttributeValue(NAMESPACE, "deson");

		Log.i(tag, mDestitle);
		Log.i(tag, mDesoff);
		Log.i(tag, mDeson);
	}

	/**
	 * 判断是否开启的方法
	 * @return	返回当前SettingItemView是否选中状态	true开启(checkBox返回true)	false关闭(checkBox返回true)
	 */
	public boolean isCheck(){
		//由checkBox的选中结果,决定当前条目是否开启
		return cb_box.isChecked();
	}

	/**
	 * @param isCheck	是否作为开启的变量,由点击过程中去做传递
	 */
	public void setCheck(boolean isCheck){
		//当前条目在选择的过程中,cb_box选中状态也在跟随(isCheck)变化
		cb_box.setChecked(isCheck);
		if(isCheck){
			//开启
			tv_des.setText(mDeson);
		}else{
			//关闭
			tv_des.setText(mDesoff);
		}
	}

}

注意事项:当我们需要重写一个自定义控件的自定义属性的时候。

我们需要在这个工程的values文件下面创一个attrs.xml

————————————————————————————————

6,自定义对话框

这里我们同理用spUtil来保存密码,将key设置为常量类里面的常量。即可

自定义对话框步骤:

第一步我们new出对话框的Builder对象。

然后因为我们要定义自己的布局对话框界面。

所以第二步我们要创建一个对话框。

第三步就是调用setView方法,去使用一个布局文件。

就是使用xml---->view对象(使用infate)

技巧:TextUtils.isEmpty(str) 这个方法可以判断str是否为""(空字符串) 或者为null

这里会出现button空指针异常。因为你直接使用findViewById是 在当前控件指定的布局文件中去找控件。

但是你现在要使用的button 是在view中的。所以我们需要使用view.findviewbyid.

	/**
	 * 设置密码对话框
	 */
	private void showSetPsdDialog() {
		//因为需要去自己定义对话框的展示样式,所以需要调用dialog.setView(view);
		//view是由自己编写的xml转换成的view对象xml----->view
		Builder builder = new AlertDialog.Builder(this);
		final AlertDialog dialog = builder.create();

		final View view = View.inflate(this, R.layout.dialog_set_psd, null);
		//让对话框显示一个自己定义的对话框界面效果
		dialog.setView(view);
		dialog.show();

		Button bt_submit = (Button) view.findViewById(R.id.bt_submit);
		Button bt_cancel = (Button) view.findViewById(R.id.bt_cancel);

		bt_submit.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				EditText et_set_psd = (EditText) view.findViewById(R.id.et_set_psd);
				EditText et_confirm_psd = (EditText)view.findViewById(R.id.et_confirm_psd);

				String psd = et_set_psd.getText().toString();
				String confirmPsd = et_confirm_psd.getText().toString();

				if(!TextUtils.isEmpty(psd) && !TextUtils.isEmpty(confirmPsd)){
					if(psd.equals(confirmPsd)){
						//进入应用手机防盗模块,开启一个新的activity
						Intent intent = new Intent(getApplicationContext(), TestActivity.class);
						startActivity(intent);
						//跳转到新的界面以后需要去隐藏对话框
						dialog.dismiss();

						SpUtil.putString(getApplicationContext(), ConstantValue.MOBILE_SAFE_PSD, psd);
					}else{
						ToastUtil.show(getApplicationContext(),"确认密码错误");
					}
				}else{
					//提示用户密码输入有为空的情况
					ToastUtil.show(getApplicationContext(), "请输入密码");
				}
			}
		});

		bt_cancel.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				dialog.dismiss();
			}
		});
	}

Eclipse快捷键:Ctrl+D 快速删除一行代码。

6,密码加密(了解)

md5加密:将字符串转换成
32位的字符串(16进制字符(0~f)) 不可逆

import java.beans.Encoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Util {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		//加盐
		String psd = "123"+"abc";
		encoder(psd);
	}

	/**给指定字符串按照md5算法去加密
	 * @param psd	需要加密的密码
	 */
	private static void encoder(String psd) {
		try {
			//1,指定加密算法类型
			MessageDigest digest = MessageDigest.getInstance("MD5");
			//2,将需要加密的字符串中转换成byte类型的数组,然后进行随机哈希过程
			byte[] bs = digest.digest(psd.getBytes());
//			System.out.println(bs.length);
			//3,循环遍历bs,然后让其生成32位字符串,固定写法
			//4,拼接字符串过程
			StringBuffer stringBuffer = new StringBuffer();
			for (byte b : bs) {
				int i = b & 0xff;
				//int类型的i需要转换成16机制字符
				String hexString = Integer.toHexString(i);
//				System.out.println(hexString);
				if(hexString.length()<2){
					hexString = "0"+hexString;
				}
				stringBuffer.append(hexString);
			}
			//5,打印测试
			System.out.println(stringBuffer.toString());
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
	}
}

java工程

123 202cb962ac59075b964b07152d234b70

时间: 2024-10-19 21:36:49

Android 安全卫士 第二天_注意事项的相关文章

二、Android学习第二天——初识Activity(转)

(转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 一. Android学习第二天——初识Activity 昨天程序搭建成功以后,就可以开发跟运行Android应用程序了,因为Activity是开发中不可或缺的组成部分,所以要对Activity有所认识. 以下两点是需要注意的:(个人总结) 凡是覆写得方法,在方法体中的第一行一定是super.XXX(),一定要先调用父类里的相应方法做必要的事情,再根据自己的需求去写其他的代

android内部培训视频_第五节(1)_OA实战之登录界面

第五节(1):OA实战之登录界面  一.登录界面布局 1.背景图片 2.文本框 3.checkbox 4.按钮 暂未实现点击切换图片效果 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent&q

Android——Vold磁盘挂载_主体构建(一)

这段时间为了把mmc的一个block当成sdcard内置,学习了下android的vold磁盘挂载模块,记录一下(android 4.2.2). 撰写不易,转载请注明出处:http://blog.csdn.net/jscese/article/details/38068441 一:Vold的编译及启动 vold的源码位置在android根目录 /system/vold文件下,先看这下面的android.mk: common_src_files := VolumeManager.cpp Comma

android内部培训视频_第三节(3)_常用控件(ViewPager、日期时间相关、ListView)

第三节(2):常用控件之ViewPager.日期时间相关.ListView  一.ViewPager 实例:结合PagerAdapter滑动切换图片  二.日期时间相关:AnalogClock\DigitalClock\DatePicker\TimerPicker\DatePickerDialog\TimePickerDialog 三.ListView 实例1:城市选择器 实例2:自定义列表项 百度网盘视频下载地址:http://pan.baidu.com/s/1c0ip6la android内

最新Android SDK_API_开发包_离线包_下载

[转载][资源]最新Android SDK_API_开发包_离线包_下载 开发Android应用少不了Android SDK,由于谷歌服务器的原因,在国内下载Android SDK速度非常慢,如果要把所有的Android SDK下载回来势必是一件非常痛苦的事情.因此,在这里把本人下载的所有的Android SDK打包分享,希望对广大Android 开发者能有所帮助. 所安装的Android SDK,  包括谷歌到目前为止发布的所有的Android API,和部分工具. SDK_API_开发包_离

Android实现QQ分享及注意事项

一.获取APPID和帮助文档 在前面我介绍了关于Android中微信分享的文章< Android实现微信分享及注意事项>这一篇文章来看看关于QQ分享. 可以参看新手引导和接入说明:http://wiki.open.qq.com/wiki/移动应用接入wiki索引 分享相关文档说明:http://wiki.open.qq.com/index.php?title=Android_API调用说明&=45038#1.13_.E5.88.86.E4.BA.AB.E6.B6.88.E6.81.AF

android内部培训视频_第四节(1)_异步网络操作

第四节(1):异步网络操作  一.结合asyncTask下载网络图片 1.定义下载类,继承自asyncTask,参数分别为:String(url地址),Integer(刻度,本例没有用到),BitMap(下载成功后的图片) public class downloadImageTask extends AsyncTask<String, Integer, Bitmap> { /** * 在线程开始之前执行 */ @Override protected void onPreExecute() {

关于升级到Android Studio3.2版本的注意事项

关于升级到Android Studio3.2版本的注意事项: 1.默认最低的Build Tools version 为 28.0.22.如果程序中使用了kotlin插件,需要将kotlin插件的最低版本号改为1.2.513.如果在gradle.properties文件中加入了android.overridePathCheck=true 设置,现在是不支持的要删除或注释掉 编译器给出的提示原文:1.The specified Android SDK Build Tools version (27.

C#_.NetCore_WebAPI项目_EXCEL数据导出(ExcelHelper_第二版_优化逻辑)

原文:C#_.NetCore_WebAPI项目_EXCEL数据导出(ExcelHelper_第二版_优化逻辑) 项目需要引用NPOI的Nuget包:DotNetCore.NPOI-v1.2.2 本篇文章是对WebAPI项目使用NPOI操作Excel时的帮助类:ExcelHelper的改进优化做下记录: 备注:下面的帮助类代码使用的文件格式为:xlsx文件,xlsx相对xls的优缺点代码里有注释,推荐使用xlsx文件保存数据! using Microsoft.AspNetCore.Mvc; usi