魅族/锤子/苹果 悬停效果的实现
魅族/锤子/苹果 悬停效果的实现
一、背景:近日研究当前主流手机的单手操作效果。
一类是小米的单手小屏模式:将原本5寸以上的屏幕缩小到3.5/4寸的大小,以方便单手操作
另外一类是魅族/锤子/苹果的 悬停效果:屏幕可以下拉到下半部分,这样单手可以方便的操作到屏幕上方区域
二、关于DecorView的基本概念
一、DecorView为整个Window界面的最顶层View。
二、DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。
三、LinearLayout里有两个FrameLayout子元素。
(20)为标题栏显示界面。只有一个TextView显示应用的名称。也可以自定义标题栏,载入后的自定义标题栏View将加入FrameLayout中。
(21)为内容栏显示界面。就是setContentView()方法载入的布局界面,加入其中。
DecorView的创建一般是在setContentView时完成的,具体源码在PhoneWindow的setContentView()中
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.悬停效果
执行:
mHoverLayout.move(0,mHoverLayout.getHeight()/3,true);
恢复:
mHoverLayout.goHome(true);
可以根据软件的设计采用按键/悬浮球/下滑等方式来触发悬停执行的代码
五、悬停效果的导入
1、单个Activity中导入
只需要在Activity的setContentView后执行下面方法,就可以实现悬停的效果。
initHoverLayout(); attachDecorToFlyingLayout();
2.系统导入
系统导入需要修改Android的源码。导入方法和单个Activity的导入类似。由于所有Window(Activity/Toast/Dialog)的setContentView,最终调用的都是Window类的setContentView。而Window类的实现类PhoneWindow类。因此我们在PhoneWindow类中的setContentView方法后执行下面方法即可。
initHoverLayout(); attachDecorToFlyingLayout();
参考资料:http://blog.csdn.net/sunny2come/article/details/8899138 Android DecorView浅析
《Android开发艺术探索》
https://github.com/tkgktyk/FlyingLayout