魅族/锤子/苹果 悬停效果的实现
一、背景:近日研究当前主流手机的单手操作效果。
一类是小米的单手小屏模式:将原本5寸以上的屏幕缩小到3.5/4寸的大小,以方便单手操作
另外一类是魅族/锤子/苹果的 悬停效果:屏幕可以下拉到下半部分,这样单手可以方便的操作到屏幕上方区域
二、关于DecorView的基本概念

一、DecorView为整个Window界面的最顶层View。
二、DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。
三、LinearLayout里有两个FrameLayout子元素。
(20)为标题栏显示界面。只有一个TextView显示应用的名称。也可以自定义标题栏,载入后的自定义标题栏View将加入FrameLayout中。
(21)为内容栏显示界面。就是setContentView()方法载入的布局界面,加入其中。
DecorView的创建一般是在setContentView时完成的,具体源码在PhoneWindow的setContentView()中
|
1
|
installDecor(); |
三、悬停体验的基本设计思路:
1.获取当前Window的DecorView,并将DecorView中的所有View保存下来(其实是保存了一个LinearLayout)
2.设计一个有滚动效果的Layout——HoverLayout,支持整体Move
3.将之前从DecorView中保存下来的View,addView到第二步中有滚动效果的HoverLayout中去。
4.DecorView.removeAllViews()
5.DecorView.addView(HoverLayout)
四、具体的代码:
1.HoverLayout的实现:
HoverLayout继承于FrameLayout,最主要的区别于FrameLayout的地方在于
a.对FrameLayout的x,y坐标做属性动画
b.onLayout中,根据FrameLayout的x,y坐标的变化,通过child.layout更新子View的坐标
1 package com.xerrard.hoverdemo;
2
3 import android.animation.TypeEvaluator;
4 import android.animation.ValueAnimator;
5 import android.content.Context;
6 import android.graphics.Point;
7 import android.graphics.Rect;
8 import android.util.AttributeSet;
9 import android.view.Gravity;
10 import android.view.View;
11 import android.view.ViewConfiguration;
12 import android.widget.FrameLayout;
13
14 /**
15 * Created by xerrard on 2015/11/3.
16 */
17 public class HoverLayout extends FrameLayout {
18 private int mDefaultTouchSlop;
19 private static int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
20 private static final float DEFAULT_SPEED = 1.0f;
21 private int mOffsetX = 0;
22 private int mOffsetY = 0;
23 private Rect mChildRect;
24
25 public HoverLayout(Context context, AttributeSet attrs, int defStyle) {
26 super(context, attrs, defStyle);
27 initialize();
28 fetchAttribute(context, attrs, defStyle);
29 }
30
31 public HoverLayout(Context context, AttributeSet attrs) {
32 this(context, attrs, 0);
33 }
34
35 public HoverLayout(Context context) {
36 super(context);
37 initialize();
38 }
39
40 private void fetchAttribute(Context context, AttributeSet attrs, int defStyle) {
41 }
42
43 private void initialize() {
44 mDefaultTouchSlop = ViewConfiguration.get(getContext())
45 .getScaledTouchSlop(); //获取滑动的最小距离
46 mChildRect = new Rect();
47 }
48
49 @Override
50 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
51 layoutChildren(left, top, right, bottom, false /* no force left gravity */);
52 }
53
54 void layoutChildren(int left, int top, int right, int bottom,
55 boolean forceLeftGravity) {
56 final int count = getChildCount();
57
58 final int parentLeft = getPaddingLeft();
59 final int parentRight = right - left - getPaddingRight();
60
61 final int parentTop = getPaddingTop();
62 final int parentBottom = bottom - top - getPaddingBottom();
63
64 for (int i = 0; i < count; i++) {
65 final View child = getChildAt(i);
66 if (child.getVisibility() != GONE) {
67 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
68
69 final int width = child.getMeasuredWidth();
70 final int height = child.getMeasuredHeight();
71
72 int childLeft;
73 int childTop;
74
75 int gravity = lp.gravity;
76 if (gravity == -1) {
77 gravity = DEFAULT_CHILD_GRAVITY;
78 }
79
80 final int layoutDirection = getLayoutDirection();
81 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
82 final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
83
84 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
85 case Gravity.CENTER_HORIZONTAL:
86 childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
87 lp.leftMargin - lp.rightMargin;
88 break;
89 case Gravity.RIGHT:
90 if (!forceLeftGravity) {
91 childLeft = parentRight - width - lp.rightMargin;
92 break;
93 }
94 case Gravity.LEFT:
95 default:
96 childLeft = parentLeft + lp.leftMargin;
97 }
98
99 switch (verticalGravity) {
100 case Gravity.TOP:
101 childTop = parentTop + lp.topMargin;
102 break;
103 case Gravity.CENTER_VERTICAL:
104 childTop = parentTop + (parentBottom - parentTop - height) / 2 +
105 lp.topMargin - lp.bottomMargin;
106 break;
107 case Gravity.BOTTOM:
108 childTop = parentBottom - height - lp.bottomMargin;
109 break;
110 default:
111 childTop = parentTop + lp.topMargin;
112 }
113
114 //child.layout(childLeft, childTop, childLeft + width, childTop + height);
115 mChildRect.set(childLeft, childTop, childLeft + width, childTop
116 + height);
117 mChildRect.offset(mOffsetX, mOffsetY);
118 child.layout(mChildRect.left, mChildRect.top, mChildRect.right,
119 mChildRect.bottom);
120 }
121 }
122 }
123
124 protected int clamp(int src, int limit) {
125 if (src > limit) {
126 return limit;
127 } else if (src < -limit) {
128 return -limit;
129 }
130 return src;
131 }
132
133 public void moveToHalf() {
134 move(0, getHeight() / 2, true);
135 }
136
137 public void move(int deltaX, int deltaY, boolean animation) {
138 deltaX = (int) Math.round(deltaX * DEFAULT_SPEED);
139 deltaY = (int) Math.round(deltaY * DEFAULT_SPEED);
140 moveWithoutSpeed(deltaX, deltaY, animation);
141 }
142
143 public void moveWithoutSpeed(int deltaX, int deltaY, boolean animation) {
144 int hLimit = getWidth();
145 int vLimit = getHeight();
146 int newX = clamp(mOffsetX + deltaX, hLimit);
147 int newY = clamp(mOffsetY + deltaY, vLimit);
148 if (!animation) {
149 setOffset(newX, newY);
150 } else {
151 Point start = new Point(mOffsetX, mOffsetY);
152 Point end = new Point(newX, newY);
153 /*带有线性插值器(针对x/y坐标)的属性(Point)动画*/
154 ValueAnimator anim = ValueAnimator.ofObject(
155 new TypeEvaluator<Point>() {
156 @Override
157 public Point evaluate(float fraction, Point startValue,
158 Point endValue) {
159 return new Point(Math.round(startValue.x
160 + (endValue.x - startValue.x) * fraction),
161 Math.round(startValue.y
162 + (endValue.y - startValue.y)
163 * fraction));
164 }
165 }, start, end);
166 anim.setDuration(250);
167 /*监听整个动画过程,每播放一帧动画,onAnimationUpdate就会调用一次*/
168 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
169 @Override
170 public void onAnimationUpdate(ValueAnimator animation) {
171 /*获得动画播放过程中的Point当前值*/
172 Point offset = (Point) animation.getAnimatedValue();
173 setOffset(offset.x, offset.y);//根据当前Point值去requestLayout
174 }
175 });
176 anim.start();
177 }
178 }
179
180 public void setOffsetX(int offset) {
181 mOffsetX = offset;
182 requestLayout();
183 }
184
185 public int getOffsetX() {
186 return mOffsetX;
187 }
188
189 public void setOffsetY(int offset) {
190 mOffsetY = offset;
191 requestLayout();
192 }
193
194 public int getOffsetY() {
195 return mOffsetY;
196 }
197
198 public void setOffset(int x, int y) {
199 mOffsetX = x;
200 mOffsetY = y;
201 requestLayout();
202 }
203
204 public void goHome(boolean animation) {
205 moveWithoutSpeed(-mOffsetX, -mOffsetY, animation);
206 }
207
208 }
2.DecorView中的View放到HoverLayout中,然后将HoverLayout更新到DecorView的代码
1 private void initHoverLayout() {
2 // setup ContainerView
3 mContainerView = new FrameLayout(this);
4 mContainerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams
5 .MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
6
7 // setup HoverLayout
8 mHoverLayout = new HoverLayout(this);
9 mHoverLayout.addView(mContainerView);
10 mHoverLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams
11 .MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
12
13
14 }
1 private void attachDecorToHoverLayout() {
2 ViewGroup decor = (ViewGroup) getWindow().peekDecorView();
3 Drawable bg= decor.getBackground();
4 List<View> contents = new ArrayList<View>();
5 for (int i = 0; i < decor.getChildCount(); ++i) {
6 contents.add(decor.getChildAt(i));
7 }
8 decor.removeAllViews();
9
10 FrameLayout backgroud = new FrameLayout(this);
11 backgroud.setBackground(bg);
12 mContainerView.addView(backgroud);
13 for (View v : contents) {
14 mContainerView.addView(v, v.getLayoutParams());
15 }
16 mHoverLayout.setBackground(WallpaperManager.getInstance(this).getDrawable());
17 decor.addView(mHoverLayout);
18 }
3.悬停效果
执行:
|
1
|
mHoverLayout.move(0,mHoverLayout.getHeight()/3,true); |
恢复:
|
1
|
mHoverLayout.goHome(true); |
可以根据软件的设计采用按键/悬浮球/下滑等方式来触发悬停执行的代码
五、悬停效果的导入
1、单个Activity中导入
只需要在Activity的setContentView后执行下面方法,就可以实现悬停的效果。
|
1
2
|
initHoverLayout();attachDecorToFlyingLayout(); |
2.系统导入
系统导入需要修改Android的源码。导入方法和单个Activity的导入类似。由于所有Window(Activity/Toast/Dialog)的setContentView,最终调用的都是Window类的setContentView。而Window类的实现类PhoneWindow类。因此我们在PhoneWindow类中的setContentView方法后执行下面方法即可。
|
1
2
|
initHoverLayout();attachDecorToFlyingLayout(); |
参考资料:http://blog.csdn.net/sunny2come/article/details/8899138 Android DecorView浅析
《Android开发艺术探索》
浙公网安备 33010602011771号