Android动画转场的本质

我们进行转场动画最常用的就是Translation。我们查看源代码知道Translation的父类是Visibility。我们在自定义转场动画的时候只需要实现onAppear() 和 onDisappear() 方法。只需要在这里面去创建对应的Animator对象,实现各自的动画就好了。现有的实现类有Fade,Slide,Explode

View有自带的做缩放平移旋转…..的api:

​ 这些动画的底层实现都是交给了RenderNode来做。

1
2
3
4
5
6
public void setScaleY(float scaleY) {
mRenderNode.setScaleY(scaleY);
}
public void setRotationY(float rotationY) {
mRenderNode.setRotationY(rotationY);
}

通过上面这个方法修改view的属性,他的点击事件也会跟随view走。

补间动画不能改变触摸事件的区域,但是属性动画可以。但是为什么可以呢?

通过查看属性动画的源码得知:

1
2
3
4
5
6
public float getX() {
return mLeft + getTranslationX();
}
public float getY() {
return mTop + getTranslationY();
}

我们获取x 和 y的时候都分别加上了对应的Translation,而属性动画更新的就是这个值。所以通过getX getY可以获取到正确的值。

那问题又来了,如果说坐标指的是left,top,right,bottom,那就不对了。通过上面分析可以看到,我们在做动画的时候只更改了getX 和 getY的值,而并没有去修改这四个值。我们可以打开开发者选项里的显示布局边界:

代码:

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
mView.setOnTouchListener(new View.OnTouchListener() {

int lastX, lastY;
Toast toast = Toast.makeText(TestActivity.this, "", Toast.LENGTH_SHORT);

@Override
public boolean onTouch(View v, MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();

if (event.getAction() == MotionEvent.ACTION_MOVE) {
//Toolbar和状态栏的高度
int toolbarHeight = (getWindow().getDecorView().getHeight() - findViewById(R.id.root_view).getHeight());
int widthOffset = mView.getWidth() / 2;
int heightOffset = mView.getHeight() / 2;

mView.setTranslationX(x - mView.getLeft() - widthOffset);
mView.setTranslationY(y - mView.getTop() - heightOffset - toolbarHeight);

toast.setText(String.format("left: %d, top: %d, right: %d, bottom: %d",
mView.getLeft(), mView.getTop(), mView.getRight(), mView.getBottom()));
toast.show();
}
lastX = x;
lastY = y;
return true;
}
});
img1

可以发现,当view移动的时候,哪个框框并没有移动,打印的left值也没变。

那我们直接改layout:

1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean onTouch(View v, MotionEvent event) {
...
if (event.getAction() == MotionEvent.ACTION_MOVE) {
...
mView.layout(x - widthOffset, y - heightOffset - toolbarHeight,
x + widthOffset, y + heightOffset - toolbarHeight);
...
}
return true;
}
img1

可以发现用layout移动view,那个框框也是会动的。

还有一个实现改变view的位置的方法:这个方法也不是正真改变控件的位置。

1
2
view.offsetTopAndBottom(offectY)
view.offsetLeftAndRight(offectX)

那现在问题来了。既然setTranslation没有改变正真的坐标,那为什么触摸的区域确会跟着移动呢?

Android实现圆弧滑动效果之ArcSlidingHelper篇_陈小缘的博客-CSDN博客

这里我们就需要看事件分发的代码了:

事件传递顺序:

img1

先从Activity传到PhonwWindow才传到Decorview,DecorView是继承自ViewGroup,然后就走到了ViewGroup的dispatchTouchEvent(Event e)方法。

进入viewGroup首先判断这个view是不是可见 and 有没有被遮挡:

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
//ViewGroup 判断这个view是不是可见 and 有没有被遮挡:
if (onFilterTouchEventForSecurity(ev)) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 重置整个调用链子的状态 ViewGroup里面有一个参数 mFirstTouchTarget 保存了整个调用链的数据,在第一down事件的时候他为null
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 可以看到调用 onInterceptTouchEvent方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
}

// 判断点击的点是不是在这个控件范围l
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}

手指按下屏幕的时候,首先进入viewgroup的dispatchTouchEvent(),在onInterceptTouchEvent方法里面判断是否拦截,如果不拦截,开始到序的方式遍历viewGroup里面的全部子view。遍历的过程中,依次判断手指按下的坐标,是否在当前子view的区间里面,如果不在里面,就continue,开始下一个子view的判断。当找到合适的时候,执行dispatchTransformedTouchEvent()方法,看看子view是否处理这个事件,在里面执行了child.dispatchTouchEvent(event);方法。如果这个child是一个viewgroup方法,那么会再次执行上面这一串逻辑,如果是view,那么我们进入view的dispatchTouchEvent方法。在进入view的dispatchTouchEvent方法我们可以看到如果这个view设置了setOnTouchListener方法,那么点击事件就会传给这个回调里面,就不会走setOnClickListener设置的点击事件,如果没设置setOnTouchListener,那么就会接着执行view的onTouchEvent方法。

这个child.dispatchTouchEvent(event)返回true的时候代表这次的down事件有人处理了。

然后方法继续回到viewGroup的dispatchTouchEvent方法,给newTouchTargets附上当前这个view的值,然后mFirstTouchTarget赋值 = newTouchTargets,然后告诉这次的事件找到了执行者 alreadyDispatchedToNewTouchTarget = true

TransitionManagers实现View变化的过度动画

使用TransitionManager实现view的过度效果:

图片替换文本

实现起来很简单,只需要在修改view位置的时候调用下面这个方法就好了:

1
2
3
var transition = ChangeBounds()
transition.duration = 500
TransitionManager.beginDelayedTransition(binding.con, transition)

字节跳动基于Scene实现的页面跳转框架:bytedance/scene: Android Single Activity Applications framework without Fragment. (github.com)

kotlin1.5.0

密封类,密封接口

​ 密封类,密封接口就是一种专门用来配合when语句使用得到类,举个例子,假如在Android中我们有一个view,我们需要动态的设置他的显示和影藏,或者,动态设置他的偏移,可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
sealed class UiOP{
object Show:Uiop()
object Hide:Uiop()
class TranslateX(val px:Float):Uiop()
class TranslateY(val px:Float):UiOp()
}
fun execute(view:View,op:Uiop) = when(op){
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
is UiOp.TranslateX -> view.translationX = op.px // 这个 when 语句分支不仅告诉 view 要水平移动,还告诉 view 需要移动多少距离,这是枚举等 Java 传统思想不容易实现的
is UiOp.TranslateY -> view.translationY = op.px
}

takeIf and takeUnless

官方网址:Scope functions | Kotlin (kotlinlang.org)

takeIf:如果匹配为true那么就返回对象本身,否则返回null

takeUnless:如果不匹配就返回对象本身,否则返回null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import kotlin.random.*
/**
* 当takeIf里面的表达式返回true 那么就返回自身 否则就返回null
* 当takeUnless里面的表达式返回false 那么就返回本身 否则返回null
*/
fun main() {
val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
}
// even: 76, odd: null
// even: null, odd: 29
// even: 12, odd: null

项目里面我第一使用的场景是,在使用kotlin的代理模式+DataBind的时候,我需要检测参数是否带有Bindable注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal fun getBindingIdByProperty(property: KProperty<*>): Int {
val bindingProperty = property.takeIf {
// 检测是否带了注解
it.getter.hasAnnotation<Bindable>()
}
?: throw IllegalArgumentException("KProperty: ${property.name} must be annotated with the `@Bindable` annotation on the getter.")
//没带的话我直接抛出了个异常 demo见:PokexActivity
val propertyName = bindingProperty.name.decapitalize(Locale.ENGLISH)
val bindingPropertyName = propertyName
.takeIf { it.startsWith(JAVA_BEANS_BOOLEAN) }
?.replaceFirst(JAVA_BEANS_BOOLEAN, String())
?.decapitalize(Locale.ENGLISH) ?: propertyName
return bindingFieldsMap[bindingPropertyName] ?: BR._all
}

内存学习

程序不在使用一块内存的时候,就会被释放回堆中。

  • 堆内存: 保存对象的属性内容。堆内存需要用new关键字来分配空间。
  • 栈内存: 保存堆内存的地址。

s

s s

方法区:存储虚拟机加载的内存信息,常量,静态变量,即时编译器编译后的代码等数据。

丝滑的软键盘弹出效果

效果如图:

图片替换文本

思路:首先需要禁用系统的软件盘对布局的影响:使用adjustResize的话,软键盘弹出后整个页面就不会被软禁盘推上去。

1
android:windowSoftInputMode="adjustResize|stateHidden"

需求:点击更多,弹出更多页面。

这里我是通过点击更多,移动底部view进行平移动画,在平移的时候,需要计算recycleview当前的高度,如果展示更多的页面上移后会遮挡recycleview,那么就要平移对应的高度,实现不遮挡recycleview。

弹起软键盘后,我们的焦点需要显示在recycleview的上面,这里就需要在布局文件里面加上android:fitsSystemWindows=”true” 这个属性,这样我们的edittext就会自动跑到软件的上面。然后还要在设置我们的recycleview:动态设置stackFromEnd这个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
binding.rvChatRvView.viewTreeObserver.addOnGlobalLayoutListener {
val canScroll =
binding.rvChatRvView.computeVerticalScrollRange() > binding.rvChatRvView.computeVerticalScrollExtent()
l?.stackFromEnd?.let {
if (canScroll && !it) {
l?.stackFromEnd = true
return@addOnGlobalLayoutListener
}

if (!canScroll && it) {
l?.stackFromEnd = false
}
}
}

C盘定期清理

  1. java_error_in_studio.hprof文件:C:\Users\11699\java_error_in_studio.hprof

git使用总结

git stash

先暂时对当前做的操作进行缓存,切到其他分支可以使用 git stash pop 进行恢复。

更多见博客:git stash 用法总结和注意点 - 加个小鸡腿 - 博客园 (cnblogs.com)

更新本地Android Studio的仓库列表:

1
git fetch
image-20200424132213662

查看全部branch

1
git branch -a

查看远程brandh

1
git branch -r

查看本地

1
git branch -l

创建分支

1
git branch branchname

切换分支

1
git checkout branchName

提交代码到某个分支

1
git push origin branch-name

提交代码错误时需要先将远程的代码合并到当前文件

1
git pull --rebase origin bransh-name

指定合并某一分支的代码

1
git merge bransh-name

查看已经存在的远程分支

1
git remote
在这里插入图片描述
1
git remote -v|--version 列出详细信息
在这里插入图片描述
1
git remote add 【myname】url   添加一个远程仓库
1
git remote remove origin      删除一个仓库

查看此分支是从哪个分支创建来的

1
git reflog --date=local --all | findstr 分支名

git config的相关配置:git config配置 - fireporsche - 博客园 (cnblogs.com)

查看提交日志信息:

1
2
3
4
git log 
git log --pretty=oneline
git log --online
git reflog

回退:

1
git reset --hard 324sr4

比对:

1
git diff HEAD 某某文件

分支合并

1
git merge branchName

查看用户提交信息

1
git log --author = "yu"

删除文件

1
2
git rm demo2.html

文件重命名

1
git mv 改动之前的名字.html 改动之后的名字.html

移动文件到其他位置

1
git mv 要移动的文件 即将存放文件的文件夹

查看文件变化

1
2
3
4
git log --pretty=oneline home/home.html
git show 上面一条指令执行后得到的id
或者直接用下面这命名
git log -p home.tml

操作失误一键还原

1
2
git diff
git checkout -- 要还原的文件名 //还原到上一次提交的状态

不在文件追踪如何撤销追踪操作

1
2
git reset HEAD  文件名
git checkout -- 文件名

回到上一个版本或者指定版本

1
2
3
git reset --hard HEAD^   //一个^代表一个版本
或者
git reset --hard id

只想要一个文件回退到某个版本

1
git checkout id -- 需要回退的文件名字

标签

1
2
3
4
5
6
7
8
创建:git tag [标签名] -m [备注信息] [提交版本号]
git tag v1.0 -m '第一个版本v1.0'
查看:git tag
删除:git tag -d [标签名]
推送:git push [主机名] --tags
git push origin --tags
删除远程分支:git push [主机名] :refs/tags/[标签名]
git push origin :refs/tags/v1.0

git rebase: 变基

谷歌应用下载破解方式

  1. 用谷歌浏览器打开链接:https://chrome.google.com/webstore/category/extensions?hl=zh-CN Chrome网上应用商店。搜索: apk downloader

img1

  1. 将这个插件安装到谷歌浏览器。

img2

  1. 直接打开网址:chrome-extension://fgljidimohbcmjdabiecfeikkmpbjegm/apkdownload.html

    输入正确的apk应用名字或Google Play中apk下载网址,再点击【Generate Download Link】按钮,即可生成Google Play真正的apk下载地址。

    img3

  2. 我们还需在手机上安装这个插件:apkinstaller.apk后才能安装从上面那个网址下载的软件。

    https://static.androidcontents.com/apkinstaller.apk

    img4

    1. 完成!

MarkDown生成目录

第一步

使用vscode下载插件 MarkDown All

img1

第二步

然后使用vscod快捷键生成调用命令(ctrl shift p)输入 MarkDown all。就会自动为我们生成目录

img2

WebStorm新建EJS工程

NodeJs基于EJS得建立

项目整体目录如下:

img-0

1. 新建一个目录ejs_demo

进入这个目录执行代码:npm init

安装ejs依赖和渲染插件express

npm install ejs

npm install express

2. 新建app.js 通过node运行这个脚本文件,会开启一个服务,然后完成对模板文件得渲染。

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
var http=require("http");
var express=require("express");
var app=express();
var path = require('path');
// 1.在app.js的头上定义ejs:
var ejs = require('ejs');
//定义变量
var tem={
message:"我是中间部分"
};
//创建服务器
//在控制台输入node app.js启动服务器
http.createServer(app).listen(8080,function(){
console.log("服务器地址为:http://localhost:8080");
});
//挂载静态资源处理中间件,设置css或者js引用文件的静态路径
app.use(express.static(__dirname+"/public"));
// 或者以下这个也可以
// app.use(express.static(path.join(__dirname, 'public'), {maxAge: 36000}));
//设置模板视图的目录
app.set("views","./public/views");
//设置是否启用视图编译缓存,启用将加快服务器执行效率
app.set("view cache",true);
// 2.注册html模板引擎:
app.engine('html',ejs.__express);
//设置模板引擎的格式即运用何种模板引擎
app.set("view engine","html");
//设置路由
app.get("/index",function(req,res){
res.render("index",{title:tem.message});
});

3. 新建index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>EJS</title>
<meta name="description" content="">
<meta name="keywords" content="">
<link href="/css/main.css" rel="stylesheet">
</head>
<body>
<%- include("./header.html") %>
<h1><%=title%></h1>
<%- include("./footer.html") %>
</body>
</html>

4. 新建head.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no,
initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>我是头部</h1>
</body>
</html>

5. 新建footer.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no,
initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>我是尾部</h1>
</body>
</html>

6. 新建main.cs

1
2
3
4
5
6
7
8
h1{
width:80%;
height:100px;
background:green;
margin:20px auto;
text-align: center;
line-height:100px;
}

7. 控制台运行命名:node app.js

在浏览器打开网址:localhost:8080/index

img-1

使用WebStorm模板来新建项目

1. 打开WebStorm新建项目 按如下选项进行选定。需要选择EJS模板引擎

image-3

2. 创建完毕我们会看到如下目录

image-4

3. 运行这个项目有两种方式

* 第一种是点击右上角得绿色三角形。

  然后在浏览器输入localhost:3000

  ![image-5.png](./WebStorm/image-5.png)

* 第二种方式:命令行输入 node www

  然后在浏览器输入localhost:3000

4. 新建一个新的页面方式如下:

4.1 新建文件student.ejs

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title><%= name %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= name %></h1>
<p>Welcome to <%= name %></p>
</body>
</html>

4.1 打开index.js文件。增加对student得路由跳转

1
2
3
4
5
6
7
8
9
10
11
12
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.get('/student', function(req, res, next) {
res.render('student', { name: '张云' });
});
module.exports = router;

4.2 运行后浏览器打开:localhost:3000/student

image-6