之前参与了Launcher项目,负责样式的自定义布局部分。这部分主要涉及到的技术点是自定义布局和动画,难度适中,但略繁杂,其中有几个技术相关的难点记录如下。
介绍
布局基于二维格子:将图标显示区划分为m x n
个格子,每个图标占据一个或多个格子。自由布局样式里,格子划分得更小,每个图标的最小尺寸有规定,不能小于规定值。
图标大小可更改:不同尺寸的图标展示的信息不一样,最小的图标仅展示一个ICON,最大的图标可以展示应用丰富的信息以及操作。展示的信息是通过与应用提供的服务组件或广播来通信的。
手指长按触发拖动
下图边框表示一个ViewGroup,包含8个子View。我们希望长按某个子View,并拖动它到其他的位置,比如长按View-1后,显示能够拖动,然后拖动它到View-7的位置。
Github上应该有相关框架,可以自己实现但不需要,因为Android提供了相关API,见官方文档。其原理是创建待拖动View的副本,并跟随手指触摸坐标更改副本View的位置。使用流程为在View的长按监听回调OnLongClickListener.onLongClick(View v)
里调用开启拖动的方法View.startDragAndDrop()
,然后在目标容器中注册拖动回调监听即可触发拖动事件的回调方法OnDragListener.onDragEvent()
,之后根据坐标判断拖动结束的地方或View。
关键代码如下:
定义拖拽监听,处理drop和end(如果需要)事件
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// 拖拽监听
public class ItemDragEventListener implements View.OnDragListener {
public void addEventSender(ViewGroup cell) {
cell.setOnDragListener(this);
}
public boolean onDrag(View v, DragEvent event) {
final int action = event.getAction();
switch(action) {
case DragEvent.ACTION_DRAG_STARTED:
case DragEvent.ACTION_DRAG_ENTERED:
case DragEvent.ACTION_DRAG_LOCATION:
case DragEvent.ACTION_DRAG_EXITED:
return true;
case DragEvent.ACTION_DROP:
return onDrop(v, event);
case DragEvent.ACTION_DRAG_ENDED:
return onEnd(v, event);
default:
break;
}
return false;
}
// 处理drop事件
private boolean onDrop(View v, DragEvent event) {
// from info
View view = (View) event.getLocalState();
ViewGroup fromCell = (ViewGroup) view.getParent();
int fromIndex = fromCell.indexOfChild(view);
// current(target) info
final ViewGroup targetCell = (ViewGroup) v;
int targetIndex = Utils.getChildIndexStrictly(targetCell, event.getX(), event.getY());
// move item
if (targetIndex >= 0 && targetIndex < targetCell.getChildCount()) {
moveItem(fromCell, fromIndex, targetCell, targetIndex);
}
return true;
}
}其中,通过坐标定位目标图标View的方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static int getChildIndexStrictly(ViewGroup fl, float x, float y) {
int count = fl.getChildCount();
int left, top, right, bottom;
for (int i = 0; i < count; i ++) {
final View child = fl.getChildAt(i);
left = child.getLeft();
right = child.getRight();
top = child.getTop();
bottom = child.getBottom();
if (x >= left && x <= right && y >= top && y <= bottom) {
return i;
}
}
return -1;
}将拖拽监听设置给目标容器
1
itemDragEventListener.addEventSender(viewGroup);
在待拖拽的图标View长按监听里设置拖拽事件
1
2
3
4
5
6
7
8
9
10// 长按监听
public static class ItemLongClickListener implements OnLongClickListener {
public boolean onLongClick(View v) {
v.startDragAndDrop(null, new DragShadowBuilder(aiv), aiv, 0);
return true;
}
}
// 设置长按监听
itemView.setOnLongClickListener(new ItemLongClickListener());
拖动View到页面边界触延时连续页面切换
在一个ViewPager里,我们希望拖动当前Pager里的一个View到Pager的边界时,会自动切换Pager。但切换之后我们不希望马上连续切换,因为我们希望在切换后的Pager里,留给用户些许时间来判断是否释放掉拖动的View。比如下图中,我们拖动View-2到Pager的左边界时,我们希望它切换到第2个页面,并且留些许时间让用户考虑是否将View-2放在页面2,如果不放在页面2,用户会继续保持长按,则会继续切换到页面1。
实现:监听DragEvent.ACTION_DRAG_LOCATION
事件,获取实时坐标,判断是否需要切换Pager,如果切换则发送一个延时消息,在这个延时消息触发之前不允许再次切换Pager。
关键代码如下:
1 | // 监听DragEvent.ACTION_DRAG_LOCATION事件 |
缩放父视图而不改变子视图的动画
实现一个视图的缩放动画是简单的,默认会缩放子视图。如果我们仅仅只缩放俯视图而要保持子视图的尺寸,则如何实现呢?有两种实现方法:
- 利用属性动画ValueAnimator改变缩放值,在
AnimatorUpdateListener.onAnimationUpdate()
回调里实时更新父视图的MarginLayoutParams以及子视图的LayoutParams。这个方法在计算奇数除2时会导致1像素的损失而产生抖动现象,可以考虑使用浮点数,待以后验证 TODO。 - 同时利用属性动画ObjectAnimator对父视图和子视图同时缩放,但方向相反,如此子视图的尺寸能得到保持,动画流畅。
两种方法的关键代码如下
方法一:利用属性动画ValueAnimator改变缩放值,存在画面抖动现象
1 | private Animator getKeepChildScaleAnimator(View from, final View target) { |
方法二:同时利用属性动画ObjectAnimator对父视图和子视图同时缩放
1 | public Animator getKeepChildScaleAnimator(View from, final View target) { |
ViewPager页面间拖动子视图更改位置的动画
我们希望能在ViewPager页面间拖动并改变页面里的View的位置,比如下图中,我们希望拖动当前Pager3中View-2到Pager2的View-2位置上,那么Pager2中View-2就会被挤到View-3的位置,View-3会被挤到Pager3中的View-1的位置,我们希望View-X的位置改变都以动画展示出来,其他动画比较容易实现,难点是如何在Pager2页面展示View-3被挤到Pager3中的动画,即View-3向右离开Pager2的动画。
实现:由于最终的Pager2视图中是不会出现View-3的,因此需要使用一个额外的视图来表现View-3离开Pager2页面的动画,并在动画结束时移除View。
具体的方法也有两种:
- 在应用级的Window里新增View并展示动画,最后发现动画会闪烁。待调查动画闪烁原因 TODO
- 直接在Pager里添加View,并在动画结束时移除View。动画流程。
关键代码如下
方法一:使用窗口展示动画
1 | // 动画有闪烁,不用 |
方法二:直接在Pager里添加View
1 | // 在父容器里直接添加view,动画完后再删除,动画效果流畅 |