前言

Android 中ListView 的拖放操作和动画实现已经被 这个 DevByte 相关的样例 说明,并且也有 ListViewAnimations 这样强大的开源库进行了集成。但是,一番 Google 后,我发现基于 LinearLayout 的相关实现却不多。

然而,有时我们可能需要使用 LinearLayout 替代 ListView 来实现列表,例如不需要 ListView 的视图回收机制(比如使用 Fragment 作为列表项),或者我们需要把这个视图放在 ScrollView 中。

在使用 LinearLayout 实现拖放和动画时,实现代码相比于之前提到的 ListView 实现也需要一些变动。因为我在网络上没有找到相应的资料,所以写下这篇文章来记录这个过程。

拖放

前置阅读

Drag and Drop | Android Developers

LinearLayout 设定 View.OnDragListener 很简单,其机制在官方教程中有详细说明,在此不再赘述。

但是,官方教程中给出的样例在释放被拖动条目后只会显示一条 Toast,而一般的需求则是拖放排序。所以在参考了网上的一些文章后,我给出了下面这个简单的实现。与官方样例相比,添加的主要是在ViewGroup 中交换子视图的实现,以及将被拖动的视图作为 LocalState 传递。

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
public static void setupDragSort(View view) {
view.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(final View view, DragEvent event) {
ViewGroup viewGroup = (ViewGroup)view.getParent();
View dragView = (View)event.getLocalState();
switch (event.getAction()) {
case DragEvent.ACTION_DROP:
if (view != dragView) {
swapViewGroupChildren(viewGroup, view, dragView);
}
break;
}
return true;
}
});
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
view.startDrag(null, new View.DragShadowBuilder(view), view, 0);
return true;
}
});
}

public static void swapViewGroupChildren(ViewGroup viewGroup, View firstView, View secondView) {
int firstIndex = viewGroup.indexOfChild(firstView);
int secondIndex = viewGroup.indexOfChild(secondView);
if (firstIndex < secondIndex) {
viewGroup.removeViewAt(secondIndex);
viewGroup.removeViewAt(firstIndex);
viewGroup.addView(secondView, firstIndex);
viewGroup.addView(firstView, secondIndex);
} else {
viewGroup.removeViewAt(firstIndex);
viewGroup.removeViewAt(secondIndex);
viewGroup.addView(firstView, secondIndex);
viewGroup.addView(secondView, firstIndex);
}
}

这个实现已经可以完成拖动排序,然而界面效果却不理想:被拖动的条目没有消失,列表在拖动过程中也没有作出相应的改变。下面我们将使用 Android 的属性动画实现这种界面效果。

在拖动过程中响应更改

言归正传。为了实现拖放过程中的动画,我们的目标是使用 LinearLayout 的列表视图能够对用户的拖动实时作出相应,也就是每次当用户的拖动越过某个临界线的时候,就将列表展现为被拖动条目在这里放下时的预览。因此,需要完成的工作就是将被拖动视图的 Visibility 设置为View.INVISIBLE,此时被拖动视图参与布局计算,但不进行绘制(已经被用户拖起),再不断改变列表中各个条目的位置。

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
public static void setupDragSort(View view) {
view.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(final View view, DragEvent event) {
ViewGroup viewGroup = (ViewGroup)view.getParent();
DragState dragState = (DragState)event.getLocalState();
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
if (view == dragState.view) {
view.setVisibility(View.INVISIBLE);
}
break;
...
case DragEvent.ACTION_DRAG_ENDED:
if (view == dragState.view) {
view.setVisibility(View.VISIBLE);
}
break;
}
return true;
}
});
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
view.startDrag(null, new View.DragShadowBuilder(view), new DragState(view), 0);
return true;
}
});
}

private static class DragState {

public View view;
public int index;

private DragState(View view) {
this.view = view;
index = ((ViewGroup)view.getParent()).indexOfChild(view);
}
}

一个很自然的想法是,在用户拖动条目经过某个其他条目超过一般高度时,就将这个条目在父视图中的位置与被拖动条目互换(而不是等到用户拖动完成时)。这样就基本实现了布局系统中的改变。然而,由于在用户快速拖动时,Android 可能来不及向每个经过的视图发送消息,这种方式可能导致列表顺序的改变的问题(我在自己测试时就遇到了)。

所以在实现视图交换时,我们需要使用递归的方式进行,直到两个视图达到相邻。实现代码如下。

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
public static void setupDragSort(View view) {
view.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(final View view, DragEvent event) {
...
switch (event.getAction()) {
...
case DragEvent.ACTION_DRAG_LOCATION: {
if (view == dragState.view){
break;
}
int index = viewGroup.indexOfChild(view);
if ((index > dragState.index && event.getY() > view.getHeight() / 2)
|| (index < dragState.index && event.getY() < view.getHeight() / 2)) {
swapViews(viewGroup, view, index, dragState);
} else {
swapViewsBetweenIfNeeded(viewGroup, index, dragState);
}
break;
}
...
}
return true;
}
});
...
}

private static void swapViewsBetweenIfNeeded(ViewGroup viewGroup, int index,
DragState dragState)
{

if (index - dragState.index > 1) {
int indexAbove = index - 1;
swapViews(viewGroup, viewGroup.getChildAt(indexAbove), indexAbove, dragState);
} else if (dragState.index - index > 1) {
int indexBelow = index + 1;
swapViews(viewGroup, viewGroup.getChildAt(indexBelow), indexBelow, dragState);
}
}

private static void swapViews(ViewGroup viewGroup, final View view, int index,
DragState dragState)
{

swapViewsBetweenIfNeeded(viewGroup, index, dragState);
swapViewGroupChildren(viewGroup, view, dragState.view);
dragState.index = index;
}


动画

接下来是交换过程中动画的实现。在实现过程中,我参考了 justasm 的 DragLinearLayout 中的代码,在此表示感谢。

前置阅读

Property Animation | Android Developers

在实现动画时,我们主要利用的是 Android 的属性动画机制,涉及到的是 ViewY这个属性。

在谈及实际实现之前,值得在此提及的是 ViewLeftTopXY 的关系。LeftTop 是在视图树布局过程中按照视图层级和布局参数等得出的,表示特定视图在屏幕上被布局系统分配的位置;而 XY则是用于在实际绘制视图时定位的依据。

这种实现的好处是,通过将实际绘制时与布局时的视图位置独立起来,可以实现动画过程中视图的位移、旋转等视觉变换,而不必受到布局系统中视图定位的拘束。顺带一提,XY 其实是由 LeftTop分别加上 TRANSLATION_XTRANSLATION_Y得到的,这是因为实际上视图还是要依赖于布局才能定位。

言归正传。为了让视图位置的变化更加平滑,需要让视图的绘制位置从上一个位置渐变到下一个位置。我们在需要改变视图位置时可以通过 View.getY() 得到视图当前的绘制位置,但视图的下一个位置则需要经过下一次布局计算后才能获得。因此,我们使用一个常见的技巧,也就是利用ViewTreeObserver.OnPreDrawListener,在绘制之前获取已经计算完成的布局位置,在这时开始进行视图动画。

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
private static void swapViews(ViewGroup viewGroup, final View view, int index,
DragState dragState)
{

swapViewsBetweenIfNeeded(viewGroup, index, dragState);
final float viewY = view.getY();
swapViewGroupChildren(viewGroup, view, dragState.view);
dragState.index = index;
postOnPreDraw(view, new Runnable() {
@Override
public void run() {
ObjectAnimator
.ofFloat(view, View.Y, viewY, view.getTop())
.setDuration(getDuration(view))
.start();
}
});
}

private static int getDuration(View view) {
return view.getResources().getInteger(android.R.integer.config_shortAnimTime);
}

public static void postOnPreDraw(View view, final Runnable runnable) {
final ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (observer.isAlive()) {
observer.removeOnPreDrawListener(this);
}
runnable.run();
return true;
}
});
}

如此,我们就基本完成了拖放操作和动画的实现。效果如下:


附加:删除条目

既然写了这么多,最后再顺带给出一个删除条目及相应动画的实现。其中的 view 参数是在 viewGroup 外的一个拖放目标,用于删除。

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
public static void setupDragDelete(View view, final ViewGroup viewGroup) {
view.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(View view, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_ENTERED:
view.setActivated(true);
break;
case DragEvent.ACTION_DRAG_EXITED:
view.setActivated(false);
break;
case DragEvent.ACTION_DROP:
DragState dragState = (DragState)event.getLocalState();
removeView(viewGroup, dragState);
break;
case DragEvent.ACTION_DRAG_ENDED:
// NOTE: Needed because ACTION_DRAG_EXITED may not be sent when the drag
// ends within the view.
view.setActivated(false);
break;
}
return true;
}
});
}

private static void removeView(ViewGroup viewGroup, DragState dragState) {
viewGroup.removeView(dragState.view);
int childCount = viewGroup.getChildCount();
for (int i = dragState.index; i < childCount; ++i) {
final View view = viewGroup.getChildAt(i);
final float viewY = view.getY();
postOnPreDraw(view, new Runnable() {
@Override
public void run() {
ObjectAnimator
.ofFloat(view, View.Y, viewY, view.getTop())
.setDuration(getDuration(view))
.start();
}
});
}
}

需要注意的是,如果 LinearLayout 的高度设置为 wrap_content,则为了避免动画被视图边界剪裁,以及在ScrollView 中高度正确变化,需要手动对 LinearLayout 的高度进行动画;这同时涉及到需要覆盖 ScrollViewmeasureChild()方法来计算我们所请求的高度。我在下面的完整实现中完成了这个部分。

完整实现

在 GitHub 上浏览

This is my notebook for bash scripting.

Styling

Use Google style guide for shell.

Organization

Use functions; use a main function and call it by main "$@" at the end of the script.

Boolean variable

Use 1 or 0 literal for boolean; test by [[${bool_val} -eq 1 ]].

Local variable

Use local for function local variable; seprate declaration and assignment.

String operation

See this manual.

getopt

Always quote "$@" to preserve correct word splitting.

Read password

Use read -p <prompt> -s <variable>; echo to read password.

Use sudo

Include this function and call it before you’re going to use sudo.

1
2
3
prepare_sudo() {
sudo -v
}

Here document

Use Here document for usage(), etc.

You can utilize the following technique to write a here document as root

1
sudo tee "${profile_path}" >/dev/null <<EOF

Script name


name=$(basename “$0”)

1

## Report error

error() {
echo “${FUNCNAME[1]}: $@” >&2
}

1

## Configuration file

You can source an external configuration file as bash script. A bit unsafe though.

```bash
source example.conf

Wait for certain condition

1
2
3
4
for i in $(seq 0 "${timeout}"); do
check_something;
sleep 1;
done

Watch file changes

Use inotifywait.

Colored makepkg-like functions

1
2
3
4
5
6
7
8
9
10
11
12
msg_blue()·{
printf·"${BLUE}==>${ALL_OFF}${BOLD}·$1${ALL_OFF}n"
}

note()·{
printf·"${BLUE}==>${ALL_OFF}${YELLOW}·NOTE:${ALL_OFF}${BOLD}·$1${ALL_OFF}n"
}

ALL_OFF="$(tput·sgr0)"
BOLD="$(tput·bold)"
BLUE="${BOLD}$(tput·setaf·4)"
YELLOW="${BOLD}$(tput·setaf·3)"

前言

这是我在大一时 C 语言课程考试前总结的坑们,每次相关考试前都会复习一次。如果你发现可以补充的地方,欢迎评论。

正文

一言以概

任何看上去有简单答案的问题都藏有坑。

看见定义变量,总是注意变量是否被初始化过。

注意 if 中的等号是 = 还是 ==

if 语句没有花括号时只包含之后一行,如:

1
2
3
if (condition)
optionally_executed();
always_executed();

if else语句在没有花括号时,else总是匹配最近的if(Dangling Else),如:

1
2
3
if (a)
if (b) do_b();
else do_c();

实际上是:

1
2
3
4
5
6
7
if (a) {
if (b) {
do_b();
} else {
do_c();
}
}

switch语句在缺少 break 时将 Fall through。

求值逻辑表达式时 10。 判断逻辑表达式时, 非 00

truefalseTRUEFALSE 是合法的变量名,因为在 C 中它们不是关键字。

*p++等价于 *(p++),因后自增++ 优先级高于解引用*

注意整型除法结果被截断, int n = 2; 1 / n == 0

注意本地变量需要初始化。

注意变量作用域屏蔽,如:

1
2
int b;
void f() { int a = 0, b = 1; }

注意函数为传值,指针也是值传递,例如:

1
2
3
4
5
void swap(int *a, int *b) {
int *temp = a;
a = b;
b = temp;
}

并不影响外部。

注意 int 溢出,若易发生溢出(如阶乘操作)应使用 longdouble代替。

char a = 255,打印后值为 -1,因为 char 为有符号类型并采用补码表示,其范围为 -128 ~ 127

负数的位移操作中符号位也参与位移;左操作数为负数的右移 >> 结果由实现定义,Turbo C 为补 1。

a/ba%b 结果在 ab中有一个为负数时由实现定义,但保证 a / b * b + a % b 结果为a

过滤回车结束的输入字符串:

1
2
3
4
5
6
7
8
i = 0;
while ((ch = getchar()) != 'n') {
if (ch ...) {
continue;
}
str[i++] = ch;
}
str[i] = '';

C 语言二维数组存储采用 Row-major 方式,即在内存中存储为 row1, row2, row3…,初始化时必须给出列数以确定行中元素个数,静态变量中未初始化元素自动为 0,使用时可以将列数溢出到下一行,即偏移值计算为 row_count * row + column。

注意分号:

1
2
for (...);
do_something();

do_something只执行一次。

1
2
if (0);
do_something();

do_something一定会被执行。

sizeof()表达式的值在编译时确定。编译器不计算其中表达式的值,仅将其替换为对应类型。sizeof应用于数组时结果为 数组元素个数 * 元素大小,应用于指针时为指针变量长度(在 32 位机器上地址长度为 32,故值为 8;64 位机器上为 16)。

无论 x 是数组还是指针,在定义上编译器认为 x[3]*(x+3)是等价的。但根据 x 的类型是数组还是指针, 编译器将为 x+3sizeof(x)生成不同的代码。

类似 char [] 类型的数组名被视为指向 char 的指针,char[][]也被视为指向 char[] 的指针。

注意 &&||具有 短路求值 特性,不会执行无必要求值的表达式。

注意含中文字符的文件应保存为 GB* 编码,例如GB18030

strcpy(dst, src)中目标在前,源在后。

注意 int a, b, cc = 2a + b 非法(需要*)。

注意 x<=y<=z 意为(x<=y)<=z,意义非预期但合法。

scanf遇到空白字符截止,为输入一行可使用 scanf("[^n]",str)gets()

注意:scanf("%s", s)输入 "How are you?" 遇到空格截断,只得到How

strcpy(char *dst, char *src) {while(*dst++ = *src++); };++ 优先级高于*

[]()优先级高于 *,故int *array[1] 等价于 (int *)array[1],为 int 指针的数组;int (*p)[1] 为指向 int 数组的指针。

解读方法:以 int **a[1][2] 为例。从 a 出发,优先向右解读。(来源

  1. a is … int
  2. a is array of 1 … int
  3. a is array of 1 array of 2 … int
  4. a is array of 1 array of 2 pointers to … int
  5. a is array of 1 array of 2 pointers to poiner to int

优先级:后置自增 / 自减 、函数调用、数组元素、结构成员 > 前置自增 / 自减、正负、类型装换、 解引用 、取地址、数据类型大小、内存操作、(逻辑、位)非 > 结构成员解引用 > 乘除、取余 > 加减 > 位移 > 比较 > 等价 > (逻辑、位)与、或、异或 > 三元条件、(复合)赋值。

字符串字面值与字符常量中转义序列 ddd 可以为 1、2、3 位 8 进制数(0~7),上限为 377;xhh可以为 1、2 位十六进制数(0~F)。

转义序列列表:

转移序列 含义
a BEL
b BS
f FF
n LF
r CR
t HT
v VT
\
' '
" "
NULL
ddd 八进制
xhh 十六进制

0~255 的数字也可以作为有效的字符取值。

int i = -1; printf("%d", (unsigned int)i);打印出 -1,因为%d 为有符号整型。

使用草稿纸记录变量取值以计算程序输出,或对某些类型可理解程序意图猜测程序输出,或两者结合。

10^-6表示为1e-6

通过 memset(array,0,sizeof(array)) 可以实现数组重初始化。(需要string.h

# 代表将此后的文本变为字符串;##代表连接文本;含有 ###的宏不进行参数展开,可利用包装宏展开。(参考

宏函数定义:

1
2
3
4
#define func_name(var) 
do {
do_something();
} while (0)

调用与函数调用一致:

1
func_name(var);

scanfffloatlfdoubleprintffdoublelf 未定义。

,逗号表达式对左侧表达式求值并丢弃返回值,之后对右侧表达式求值并返回其返回值。

#define SWAP(a,b) (a)^=(b)^=(a)^=(b)可实现无临时变量的交换。

字符 ASCII
0 48
A 65
a 97

变量命名法则:[A-Za-z_][A-Za-z0-9_]*,且不含关键字。

autoregistervolatile均为 C 关键字。

007为八进制数字,注意范围为 0-70x0F 为十六进制数字。

strcat实现:

1
2
3
4
void strcat(char *str1, char *str2) {
while (*str && *++str);
while (*str1++ = *str2++);
}
相关阅读

《C 陷阱与缺陷》笔记 | 孙耀珠的博客

direct study weekly report(Jan25-Jan31)

Brief summary

  1. set up humanoid rig with standard animation pack
  2. set up humanoid rig with Andrew’s model
  3. use animation retargeting to create new animation sequence for Andrew’s model

Key activity

I use the skeleton in standard animation package. All the bones work fine with the humanoid rig(even the names are the same).

image

retargeting manager with skeleton1

Then I do the same thing with skeleton2. Most of the bones work good with the correct hierarchy while I miss some pieces of joint rotation as well as some advanced bones. I also get extra bones on the feet.

image

retargeting manager with skeleton2

After generating new animation sequence, I get a couple of new animation sequence, most of which are super wired… The fingers look like they are crushed by something, which might be caused by missing joint rotation. Also the feet look irregular bending which might be caused by extra bones.

image

crushed hands

image

bending feet

Anyway, we are not going to use these funny animation sequences.. Maybe I should go back do Daz 3D if I want to fix these skeletons.

Reference

To do

  • set up the character, blend animation sequence together
  • look through smart body