轻松自制flyme悬浮球
CouCanady
8年前
<p>去年用了一整年的MX4Pro,魅族留给我最大的印象就是悬浮球了(质量问题我就不说了),左右滑动切换应用、上拉返回桌面、下拉打开通知栏、轻触返回…,一切都那么丝滑。然而自从上半年换成了s7dege,我感觉怎么也习惯不了没有悬浮球的生活了。</p> <p>三星自己也有一个类似于悬浮球的功能,不过太过复杂,不易用,悬浮球本来就该是一个一步操作的产品,看来三星在软件设计方面还是任重而道远。于是乎我便在各大应用市场上找悬浮球,把所有排名靠前的悬浮球应用都安装试了一下,最后终于让我找到了一款几乎和flyme悬浮球相仿的app。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/cfdd76444e7428bcd44ae20b84f837e1.png"></p> <p>这款app在我手机里呆了好几个月,是我手机里除了微信之外,唯一允许自启动的应用了。很感谢这款app的开发者,不仅没有任何广告,还非常好用,完美移植了flyme自带的悬浮球功能。</p> <p>然而渐渐的,我便感觉到了一丝不舒服,那就是我每次安装了一个新app,打开后提示要赋予权限(存储、拍照)的时候,6.0的系统总会温馨的弹出一个框:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d672ae17bbaa855412a851c455f5b30b.jpg"></p> <p>然后我就必须到设置页面,花半天找到悬浮球,关掉它的 <strong>“可出现在顶部的应用程”</strong> 权限,然后才能回到app,授予权限。最后,我还得再次跑到设置页面,再花半天找到悬浮球,打开它的 <strong>“可出现在顶部的应用程”</strong> 权限。朋友啊朋友,这种体验,一次就够了,然而硬是让我体验了N次啊!</p> <p>然而有什么能难得倒程序员的呢?刚好这个周末在家无事,我决定按照自己的习惯,打造一个心目中最易用的悬浮球。</p> <h2><strong>设计</strong></h2> <h3><strong>1.UI</strong></h3> <p>UI很简单,直接用sketch切了三个圆,一个是作为背景的灰色半透明的圆,一个是中心的小圆,另外还有一个默认隐藏的大圆。</p> <h3><strong>2.功能</strong></h3> <p>因为自己的操作习惯是固定的,所以也就不需要给悬浮球添加自定义操作的功能了,直接将操作对应的功能写死即可。</p> <p>(1)单击:返回</p> <p>(2)长按:移动悬浮球</p> <p>(3)左滑右滑:打开最近应用程序</p> <p>(4)上拉:返回桌面</p> <p>(5)下拉:</p> <p>这块我最先开始定义的很简单,就是下拉通知栏,但是经过一天的使用,我又给它加了一个功能,就是保持下拉状态1.5秒,将移除悬浮球。这样你便可以很简单的移除掉悬浮球了。</p> <h2><strong>实现</strong></h2> <h3><strong>1.如何添加悬浮球到桌面</strong></h3> <p>这里首先要感谢郭霖大神的 《 Android桌面悬浮窗效果实现,仿360手机卫士悬浮窗效果》 ,这部分我参考了这篇文章,成功的将悬浮球添加到了桌面。</p> <pre> <code class="language-java">public static void addBallView(Context context) { if (mBallView == null) { WindowManager windowManager = getWindowManager(context); int screenWidth = windowManager.getDefaultDisplay().getWidth(); int screenHeight = windowManager.getDefaultDisplay().getHeight(); mBallView = new FloatBallView(context); LayoutParams params = new LayoutParams(); params.x = screenWidth; params.y = screenHeight / 2; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.gravity = Gravity.LEFT | Gravity.TOP; params.type = LayoutParams.TYPE_PHONE; params.format = PixelFormat.RGBA_8888; params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE; mBallView.setLayoutParams(params); windowManager.addView(mBallView, params); } }</code></pre> <h3><strong>2.手势判断</strong></h3> <p>这是最重要的部分了,承担着悬浮球的主要功能。</p> <p>(1)手指按下时</p> <p>按下时,隐藏小球,展现大球,并记录按下位置和按下时间。</p> <pre> <code class="language-java">case MotionEvent.ACTION_DOWN: mIsTouching = true; mImgBall.setVisibility(INVISIBLE); mImgBigBall.setVisibility(VISIBLE); mLastDownTime = System.currentTimeMillis(); mLastDownX = event.getX(); mLastDownY = event.getY(); postDelayed(new Runnable() { @Override public void run() { if (!mIsLongTouch && mIsTouching && mCurrentMode == MODE_NONE) { mIsLongTouch = isLongClick(event); } } }, LONG_CLICK_LIMIT); break;</code></pre> <p>代码最后的postDealy时干嘛使的呢?就是通过延迟300毫秒,判断是否是长按模式。其中判断长按的方法代码如下:</p> <pre> <code class="language-java">private boolean isLongClick(MotionEvent event) { float offsetX = Math.abs(event.getX() - mLastDownX); float offsetY = Math.abs(event.getY() - mLastDownY); long time = System.currentTimeMillis() - mLastDownTime; if (offsetX < mTouchSlop && offsetY < mTouchSlop && time >= LONG_CLICK_LIMIT) { //震动提醒 mVibrator.vibrate(mPattern, -1); return true; } else { return false; } }</code></pre> <p>(2)手指移动时</p> <p>这时要判断是否是处于长按状态,如果是,那么进入MOVE模式,移动悬浮球,如果不是,则判断操作手势,即下拉还是上拉等其他手势。</p> <pre> <code class="language-java">case MotionEvent.ACTION_MOVE: if (!mIsLongTouch && isTouchSlop(event)) { return true; } if (mIsLongTouch && (mCurrentMode == MODE_NONE || mCurrentMode == MODE_MOVE)) { mLayoutParams.x = (int) (event.getRawX() - mOffsetToParent); mLayoutParams.y = (int) (event.getRawY() - mOffsetToParentY); mWindowManager.updateViewLayout(FloatBallView.this, mLayoutParams); mBigBallX = mImgBigBall.getX(); mBigBallY = mImgBigBall.getY(); mCurrentMode = MODE_MOVE; } else { doGesture(event); } break;</code></pre> <p>进行手势操作的代码如下,主要是根据当前坐标与按下时记录的坐标进行计算,判断手势,并更新大球位置。</p> <pre> <code class="language-java">private void doGesture(MotionEvent event) { float offsetX = event.getX() - mLastDownX; float offsetY = event.getY() - mLastDownY; if (Math.abs(offsetX) < mTouchSlop && Math.abs(offsetY) < mTouchSlop) { return; } if (Math.abs(offsetX) > Math.abs(offsetY)) { if (offsetX > 0) { if (mCurrentMode == MODE_RIGHT) { return; } mCurrentMode = MODE_RIGHT; mImgBigBall.setX(mBigBallX + OFFSET); mImgBigBall.setY(mBigBallY); } else { if (mCurrentMode == MODE_LEFT) { return; } mCurrentMode = MODE_LEFT; mImgBigBall.setX(mBigBallX - OFFSET); mImgBigBall.setY(mBigBallY); } } else { if (offsetY > 0) { if (mCurrentMode == MODE_DOWN || mCurrentMode == MODE_GONE) { return; } mCurrentMode = MODE_DOWN; mImgBigBall.setX(mBigBallX); mImgBigBall.setY(mBigBallY + OFFSET); //如果长时间保持下拉状态,将会触发移除悬浮球功能 postDelayed(new Runnable() { @Override public void run() { if (mCurrentMode == MODE_DOWN && mIsTouching) { toRemove(); mCurrentMode = MODE_GONE; } } }, TO_APP_INDEX_LIMIT); } else { if (mCurrentMode == MODE_UP) { return; } mCurrentMode = MODE_UP; mImgBigBall.setX(mBigBallX); mImgBigBall.setY(mBigBallY - OFFSET); } } }</code></pre> <p>(3)手指抬起时</p> <p>手指抬起后,先要判断是否是长按模式,不是的话再判断是否是单击,都不是的话就根据当前状态触发对应功能。</p> <pre> <code class="language-java">case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsTouching = false; if (mIsLongTouch) { mIsLongTouch = false; } else if (isClick(event)) { AccessibilityUtil.doBack(mService); } else { doUp(); } mImgBall.setVisibility(VISIBLE); mImgBigBall.setVisibility(INVISIBLE); mCurrentMode = MODE_NONE; break;</code></pre> <h2><strong>效果</strong></h2> <p>到目前为止,悬浮球的功能就实现了,来看看使用效果如何。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/5c6d94ca83cf2a2a0972d5007073bd0e.gif"></p> <h2><strong>最后再说两句</strong></h2> <p>花了大半天,总算是大功告成了,程序员,最大的好处就是自己可以定制应用:joy:,</p> <p> </p> <p> </p>