你的位置:首页 > 操作系统

[操作系统]魅族/锤子/苹果 悬停效果的实现

魅族/锤子/苹果 悬停效果的实现

一、背景:近日研究当前主流手机的单手操作效果。

一类是小米的单手小屏模式:将原本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, childTop116             + 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             @Override157             public Point evaluate(float fraction, Point startValue,158                        Point endValue) {159               return new Point(Math.round(startValue.x160                   + (endValue.x - startValue.x) * fraction),161                   Math.round(startValue.y162                       + (endValue.y - startValue.y)163                       * fraction));164             }165           }, start, end);166       anim.setDuration(250);167       /*监听整个动画过程,每播放一帧动画,onAnimationUpdate就会调用一次*/168       anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {169         @Override170         public void onAnimationUpdate(ValueAnimator animation) {171           /*获得动画播放过程中的Point当前值*/172           Point offset = (Point) animation.getAnimatedValue();173           setOffset(offset.x, offset.y);//根据当前Point值去requestLayout174         }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.LayoutParams11         .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