从零构建Android工程

1 新建工程目录

新建文件夹Demo,用AndroidStudio打开。

新建build.gradle文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories{
jcenter()
google()
}
dependencies{
// 目前最新build插件版本 3.2.1
classpath 'com.android.tools.build:gradle:3.2.1'
}
}

构建工程后如图:

自动生成的gradle文件夹及使用的gradle不建议修改。如想调降gradle版本,建议降低build插件版本。

2 新建主module目录

新建settings.gradle文件和app文件夹,并在settings.gradle文件中添加include ':app',再次构建工程,app文件夹图标改变。

构建前:



构建后:


app目录下新建build.gradle文件,并添加如下内容:

1
2
3
4
5
apply plugin: 'com.android.application'

android{
compileSdkVersion 28 //目前最新sdk 28
}

app目录下新建 srcsrc/main文件夹,并在main文件夹中新建AndroidManifest.xml文件,添加如下内容:

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<manifest package="com.flueky.demo">

</manifest>

最后在工程build.gradle文件添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
buildscript {
repositories{
jcenter()
google()
}
dependencies{
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
// 以下是添加部分,定义全部工程的资源库
allprojects{
repositories{
jcenter()
google()
}
}

出现图中标志时,表示项目已经构建完成。添加默认启动Activity即可去掉 号。


3 添加启动Activity

  1. app/src/main目录下分别新建javares文件夹。
  2. java目录下创建包名:com.flueky.demo,并创建MainActivity类。
  3. res目录加创建layout文件夹,并创建activity_main.xml布局。
  4. AndroidManifest.xml文件注册MainActivity
  5. MainActivity添加启动intent

最终目录结构如图:

MainActivity内容:

1
2
3
4
5
6
7
8
9
10
11
12
package com.flueky.demo;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

activity_main.xml内容:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World" />
</LinearLayout>

AndroidManifest.xml内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.flueky.demo">

<application>

<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>

运行结果如图:

4 新建库module目录

新建library文件夹,并在settings.gradle文件中添加include ':library',构建后如下,注意library文件夹的标志。

同主module一样,创建AndroidManifest.xml文件和build.gradle文件。

编辑AndroidManifest.xml文件:

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<manifest package="com.flueky.library">

</manifest>

编辑build.gradle文件:

1
2
3
4
5
apply plugin: 'com.android.library'

android {
compileSdkVersion 28
}

在主module文件中,添加下面的代码进行关联。

implementation project(':library')

5 结束语

AndroidStudio自带的创建项目功能,做的很好。能够帮助初学者最快速度的创建Android工程,编写此篇博客的目的在于,能够帮助初学者们更好的了解Android项目工程结构。最后,将此篇博客献给测试小伙伴们。你们距离程序猿,只差面向对象编程了。

觉得有用?那打赏一个呗。[去打赏](/donate/)


安装AndroidStudio

公司准备招聘一批具备 Java 基础的实习生学习 Android 开发。因此,后续会出一系列的 Android 开发入门、基础、高级教程。那么,从第零步,搭建开发环境开始。由于 Android 是基于 Java 平台开发的,因此还需要[安装 Java 环境](/blog/2018-08-01)。

Read more
安装Java环境-Windows

1 下载jdk

jdk 8 下载链接

jdk 10 下载链接

2 安装jdk

双击下载下来的exe文件执行安装。安装过程截图如下:







安装后,使用快捷键 win+R 输入cmd 运行终端程序,在终端中 输入 java -version 校验安装结果。

如图所示,安装成功。

3 配置环境变量

  1. 打开系统属性
  1. 点击高级系统设置
  1. 点击环境变量
  1. 在系统变量中新建变量

新建JAVA_HOME变量,变量值是jdk安装目录。

  1. 在系统变量中选择Path变量
  1. 编辑Path变量,在变量值的末尾添加下面的内容。

C:\Program Files\Java\jdk1.8.0_181\bin;C:\Program Files\Java\jdk1.8.0_181\jre\bin

或者使用

%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin

  1. 保存退出后,再在终端中依次输入 javacjavah校验环境变量是否修改成功。

jdk 11

jdk 11 默认没有 jre 目录,需要手动生成。在 jdk 目录下执行下面的命令。

bin\jlink.exe –module-path jmods –add-modules java.desktop –output jre

jdk 8 之后,不再支持 javah 命令生成头文件,使用 javac -h 替换。

觉得有用?那打赏一个呗。[去打赏](/donate/)


Android 实现水波浪效果

讲真,这不算一个新的课题。早在几个月之前就已经有大神开源了水波浪效果的代码。由于工作关系,一直未能拜读。直至近期公司项目有需要,才决定去接触下该效果。

Read more
自定义Android键盘

前段时间改造了公司的安全键盘,是基于DialogButton自定义的。也因此借机了解下 Android 平台提供的自定义键盘接口。主要有两个类:KeyboardKeyboardView。很搞笑的是,百度出来自定义Android键盘(与自定义Android输入法不同)的文章千篇一律。

Read more
用Shell批量删除AndroidStudio build目录

多数Android程序猿都有一个痛,使用AndroidStudio开发安卓项目时,build目录占用太多存储空间。在没有版本控制工具的情况下,一些没有经验的Android开发者会将自己的程序直接打包发送给别人。这样的代码包,少则几十兆,多则数百兆,其中多数都是build目录下的文件。(使用eclipse开发时,bin 目录和gen目录与之类似。

Read more
仿QQ空间的透明标题头

1 目标

先看看QQ空间的样式。

透明背景标题头

白色背景标题头

2 思路

滚动页面时,当背景头部消失后,标题背景变成白色。即计算滑动距离,根据距离计算需要变更标题背景的时机,标题浮动在滚动视图上面。布局有两种设计方法:

方案一:

1
2
3
4
5
6
7
8
9
10
11
12
<RelativeLayout>
<ScrollView>
<!-- 滚动视图内容 -->
<LinearLayout>

</LinearLayout>
</ScrollView>
<!-- 标题 -->
<LinearLayout>

</LinearLayout>
</RelativeLayout>

优点:标题独立于滚动视图,无需处理
缺点:滚动视图拉伸时,影响一体化体验

方案二:

1
2
3
4
5
6
7
8
9
10
11
12
<ScrollView>
</RelativeLayout>
<!-- 滚动视图内容 -->
<LinearLayout>

</LinearLayout>
<!-- 标题 -->
<LinearLayout>

</LinearLayout>
<RelativeLayout>
</ScrollView>

优点:滚动视图拉伸时,标题一起下滑。
缺点:标题同滚动视图一起滑动,需要单独处理。

这里,选择方案二的理由是,解决方案二的缺点比解决方案一的缺点容易很多。

3 实现

重写ScrollView,监听滑动距离,保持标题布局不变,并根据时机改变背景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (titleView == null)
throw new IllegalStateException("titleView 不能为空");
if (headView == null)
throw new IllegalStateException("headView 不能为空");
titleView.setTranslationY(t);//这里使标题视图不随ScrollView滚动
/**
* 根据滚动距离计算
*/
if (headView.getHeight() - t < titleView.getHeight() * 1.2f) {
// 乘以倍数扩大处罚范围
if (isHeadShow && mScrollStateListener != null) {
// 标题显示到消失,才执行
mScrollStateListener.changed(!isHeadShow);
}
isHeadShow = false;
} else if (headView.getHeight() - t > titleView.getHeight() * 1.8f) {
// 乘以倍数扩大处罚范围
if (!isHeadShow && mScrollStateListener != null) {
// 标题消失到显示,才执行
mScrollStateListener.changed(!isHeadShow);
}
isHeadShow = true;
}

if (mScrollStateListener != null) {
// 计算头部视图显示的百分比
float percent = 0;
if (t <= headView.getHeight() - titleView.getHeight())
percent = 1 - t * 1.0f / (headView.getHeight() - titleView.getHeight());
else if (t < 0)
percent = 1;
else percent = 0;

/**
* 0.001 处理浮点数计算存在的误差
*/
if (Math.abs(1 - percent) < 0.001) {
lastPercent = percent;
percent = 1;
mScrollStateListener.openPercent(percent);
} else if (Math.abs(percent) < 0.001) {
lastPercent = percent;
percent = 0;
mScrollStateListener.openPercent(percent);
}
//两次变化百分比小于0.1时,不作处理
if (Math.abs(lastPercent - percent) < 0.1)
return;
mScrollStateListener.openPercent(percent);
}
}

事实告诉你,实现起来很容易,重写这一个方法就好。

由于重写这个ScrollView的目的是修改标题背景,因此headViewtitleView不能为空。它们存在的意义在于,获取它们的高度,根据高度和滑动距离计算变更标题背景的时机,和保持标题视图的稳定不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 滚动状态监听
*/
public interface ScrollStateListener {
/**
* 背景图片完全显示时,openPercent 值是 1
*/
public void openPercent(float openPercent);
/**
* 背景图片完全显示时,isOpen 值是 true
*/
public void changed(boolean isOpen);
}

changed方法中,根据isOpen就可以实现QQ空间的效果。但功能并不限于此。还可以根据openPercent方法值的变化,给标题背景设置渐变过度效果。

4 结束语

这个效果可以很方便的同当下流行的下拉刷新组件结合。详情见GitHub:flueky/Android-PullToRefresh

附上结合使用后的效果图:

觉得有用?那打赏一个呗。[去打赏](/donate/)


Java中的排序——高级用法

上一篇文章中提到,怎样造一个轮子既适用于文件的排序又适用于商品的排序。Java给我们提供了两个很强大的功能:反射、注解。

Read more
浅谈 Java 中的排序

进来不要失望,这不是一篇讲排序算法的文章。而是简要介绍 Java 中排序相关的类ComparatorComparable以及Collections.sort的使用。

Read more
优化Android Log类,并保存日志内容至文件

1 目的何在

为什么要优化log?举两个例子。

开发中遇到问题时,我们喜欢用log帮助自己分析问题,通常没有在解决问题之后删除日志输出代码的习惯,那么问题来了。别人也可以根据你开发时的日志信息来分析你程序的漏洞,所以安全公司一般建议在release包中删除日志输入代码。这时候不可能逐行删除(工作量太大)。

安卓系统更新快,机型多且杂。考虑到兼容性问题,那么至少在主流的几个OS版本和品牌手机上测试。即使是专业的IT公司有足够的设备供你测试,也相信你是希望将这份测试工作交给专业的测试人员负责。这时候出现问题,总不能叫别人把手机拿过来给你连上电脑调试。因此有必要将日志输出内存保存至文件,然后直接分析日志文件即可。

所以,接下来看如何进行封装。

2 枚举日志级别

分析原生log类,常用日志级别有6个,从依次是VERBOSE、DEBUG、INFO、WARN、ERROR、ASSERT,6个值依次对应6个整型常量。

依此生成7个枚举变量(新增CLOSE,用来生成release包时,不输出日志)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 日志级别
*/
public enum Level {

VERBOSE(Log.VERBOSE),

DEBUG(Log.DEBUG),

INFO(Log.INFO),

WARN(Log.WARN),

ERROR(Log.ERROR),

ASSERT(Log.ASSERT),

CLOSE(Log.ASSERT + 1);

int value;

Level(int value) {
this.value = value;
}
}

3 封装原生方法

封装有三个目的:

  1. 关于TAG,每次都需要申明一个静态常量或者每次写一个参数。重载函数,将参数String改成Object,这样可以直接用this关键字传递类对象并通过target.getClass().getSimpleName()获取类名称做TAG。
  2. 之前申明7个枚举变量,用作日志筛选和关闭日志输出,因此在调用原生6个日志输出函数之前添加筛选逻辑代码。
  3. 保存日志至问价有两个方式:用logcat命令(AS和eclipse的日志窗口使用)、在日志函数调用前手动保存。这里重点介绍该方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
public static final void i(String tag, String msg) {
if (currentLevel.value > Level.INFO.value)
return;
if (isWriter) {
write(tag, msg, "I");
}
Log.i(tag, msg);
}

public static final void i(String tag, String msg, Throwable throwable) {
if (currentLevel.value > Level.INFO.value)
return;
if (isWriter) {
write(tag, msg, "I", throwable);
}
Log.i(tag, msg, throwable);
}

public static final void v(String tag, String msg) {
if (currentLevel.value > Level.VERBOSE.value)
return;
if (isWriter) {
write(tag, msg, "V");
}
Log.v(tag, msg);
}

public static final void v(String tag, String msg, Throwable throwable) {
if (currentLevel.value > Level.VERBOSE.value)
return;
if (isWriter) {
write(tag, msg, "V", throwable);
}
Log.v(tag, msg, throwable);
}

public static final void d(String tag, String msg) {
if (currentLevel.value > Level.DEBUG.value)
return;
if (isWriter) {
write(tag, msg, "D");
}
Log.d(tag, msg);
}

public static final void d(String tag, String msg, Throwable throwable) {
if (currentLevel.value > Level.DEBUG.value)
return;
if (isWriter) {
write(tag, msg, "D", throwable);
}
Log.d(tag, msg, throwable);
}

public static final void e(String tag, String msg) {
if (currentLevel.value > Level.ERROR.value)
return;
if (isWriter) {
write(tag, msg, "E");
}
Log.e(tag, msg);
}

public static final void e(String tag, String msg, Throwable throwable) {
if (currentLevel.value > Level.ERROR.value)
return;
if (isWriter) {
write(tag, msg, "E", throwable);
}
Log.e(tag, msg, throwable);
}

public static final void w(String tag, String msg) {
if (currentLevel.value > Level.WARN.value)
return;
if (isWriter) {
write(tag, msg, "W");
}
Log.w(tag, msg);
}

public static final void w(String tag, String msg, Throwable throwable) {
if (currentLevel.value > Level.WARN.value)
return;
if (isWriter) {
write(tag, msg, "W", throwable);
}
Log.w(tag, msg, throwable);
}

public static final void i(Object target, String msg) {
i(target.getClass().getSimpleName(), msg);
}

public static final void i(Object target, String msg, Throwable throwable) {
i(target.getClass().getSimpleName(), msg, throwable);
}

public static final void v(Object target, String msg) {
v(target.getClass().getSimpleName(), msg);
}

public static final void v(Object target, String msg, Throwable throwable) {
v(target.getClass().getSimpleName(), msg, throwable);
}

public static final void d(Object target, String msg) {
d(target.getClass().getSimpleName(), msg);
}

public static final void d(Object target, String msg, Throwable throwable) {
d(target.getClass().getSimpleName(), msg, throwable);
}

public static final void e(Object target, String msg) {
e(target.getClass().getSimpleName(), msg);
}

public static final void e(Object target, String msg, Throwable throwable) {
e(target.getClass().getSimpleName(), msg, throwable);
}

public static final void w(Object target, String msg) {
w(target.getClass().getSimpleName(), msg);
}

public static final void w(Object target, String msg, Throwable throwable) {

w(target.getClass().getSimpleName(), msg, throwable);
}

4 保存日志内容至文件

封装原生方法的目的在于,我们可以插入write方法,保存日志内容至文件。LOG_FORMAT 是仿造AS的日志输出格式,后面会附上结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 写文件操作
*
* @param tag 日志标签
* @param msg 日志内容
* @param level 日志级别
* @param throwable 异常捕获
*/
private static final void write(String tag, String msg, String level, Throwable throwable) {
String timeStamp = LOG_TIME_FORMAT.format(Calendar.getInstance().getTime());

try {
writer.write(String.format(LOG_FORMAT, timeStamp, Process.myPid(), Process.myTid(), pkgName, level, tag));
writer.write(msg);
writer.newLine();
writer.flush();
osWriter.flush();
fos.flush();
if (throwable != null)
saveCrash(throwable);
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 保存异常
*
* @param throwable
* @throws IOException
*/
private static void saveCrash(Throwable throwable) throws IOException {
StringWriter sWriter = new StringWriter();
PrintWriter pWriter = new PrintWriter(sWriter);
throwable.printStackTrace(pWriter);
Throwable cause = throwable.getCause();
while (cause != null) {
cause.printStackTrace(pWriter);
cause = cause.getCause();
}
pWriter.flush();
pWriter.close();
sWriter.flush();
String crashInfo = writer.toString();
sWriter.close();
writer.write(crashInfo);
writer.newLine();
writer.flush();
osWriter.flush();
fos.flush();
}

保存的日志内容如下:

1
08-14 17:15:03.665 24152-24152/com.flueky.app D/TAG:838E512687D20F6B40409A2E3A7B24156774F47C

5 组件初始化

  1. 传入上下文是为了获取程序包名和程序的外部缓存目录:/sdcard/Android/data/包名/。这里的日志文件保存目录是:/sdcard/Android/data/包名/log/日志文件
  2. isWriter 标记是否需要保存日志内容至文件。
  3. level设置日志输出级别。当level 等于CLOSE时,不输出日志也不保存至文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 日志组件初始化
*
* @param appCtx application 上下文
* @param isWriter 是否保存文件
* @param level 日志级别
*/
public static final void initialize(Context appCtx, boolean isWriter, Level level) {
currentLevel = level;
if (level == Level.CLOSE) {
isWriter = false;
return;
}
Logger.isWriter = isWriter;
if (!Logger.isWriter) {//不保存日志到文件
return;
}
String logFoldPath = appCtx.getExternalCacheDir().getAbsolutePath() + "/../log/";
pkgName = appCtx.getPackageName();
File logFold = new File(logFoldPath);
boolean flag = false;
if (!(flag = logFold.exists()))
flag = logFold.mkdirs();
if (!flag) {
Logger.isWriter = false;
return;
}
logFilePath = logFoldPath + FILE_NAME_FORMAT.format(Calendar.getInstance().getTime()) + ".log";
try {
File logFile = new File(logFilePath);
if (!(flag = logFile.exists()))
flag = logFile.createNewFile();
Logger.isWriter = isWriter & flag;
if (Logger.isWriter) {
fos = new FileOutputStream(logFile);
osWriter = new OutputStreamWriter(fos);
writer = new BufferedWriter(osWriter);
}
} catch (IOException e) {
e.printStackTrace();
Logger.isWriter = false;
}
}

觉得有用?那打赏一个呗。[去打赏](/donate/)