IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

仿iPhone辅助球实现

开源实验室 2016-04-02 23:10:40 累计浏览 2,802 次
本机暂存

过年在家时,翻出来了很多去年写的代码。就挑出来了一些自我感觉良好的讲解了。

示例图片:

效果如图,demo中我并没有做完全和iphone那样展开的菜单(因为懒)。
那么接下来就讲讲这个能漂浮在任意Activity、包括手机桌面上的手势菜单是如何实现的。

正文

首先从宏观原理上来讲,它就是一个跑在Service中的View。 从细节上来讲,主要有两个问题需要我们注意:

如何让一个Service中能够显示出View来。
如何让这个View做到相应手势事件(包括点击和拖动)

第一个问题:这要从Activity开始讲起了。
我们都知道,Android中的View并不包括Activity、ActionBar、Dialog、Notification等。Activity能够显示出一个View,是因为我们调用了setContentView()这个方法。再往深层看,Activity之所以能显示View是因为Activity本身使用has…a…关系包含了一个Window对象,而这个Window对象可以理解为一个屏幕,如果你有做过IOS开发,你一定能理解这个Window的含义,其实就是代表了当前屏幕。
通过这个Window对象,我们可以获得一个DecorView对象,这个DecorView对象是一个ViewGroup就是当前屏幕的根View,一切Activity的ContentView都会被add进这个DecorView容器中。
再回到上一个问题,View是如何显示出来的。
在Window对象中,有一个内部类叫做LocalWindowManager,这个内部类实现了一个接口叫WindowManager,而就是这个LocalWindowManager在实现WindowManager.addView()接口方法的时候,实现了在屏幕上绘制View的功能。

原理说了这么多,你也许已经糊涂了,这到底和Service显示View有什么关系。  
其实很简单,因为Window对象负责显示我们的View,那么在Service中得到当前的Window对象,一切问题就解决了。  
在Context中有一个获取当前系统管理者的方法叫getSystemService(String name);我们就通过这个方法来获取到Window对象中的WindowManager而这里得到的WindowManager对象就是上文提到的LocalWindowManager对象。通过给这个对象的addView()再添加一个我们的手势球View,就解决了View显示问题。

第二个问题:让一个View随着手指移动而移动。  
有两种方法实现这种需求。  
在《疯狂Android讲义》中有一章是介绍通过自定义控件的形式,重写ondraw()方法来实现,这是在View内部封装,这里我不讲了,希望了解的可以自己去看看电子书。我实现的方法是通过外部给View设置触摸事件监听setOnTouchListener(),然后通过当前手指所在屏幕的坐标,来动态修改View的margin边距来达到View改变位置的效果。

代码

具体实现,我们来这个核心Service类

public class TopFloatService extends Service implements OnClickListener {
    WindowManager wm = null;
    WindowManager.LayoutParams ballWmParams = null;
    private float mTouchStartX;
    private float mTouchStartY;
    private float x;
    private float y;
    private View ballView; // 球状态View
    private View menuView; // 菜单状态View
    private PopupWindow pop;
    private Button floatImage;
    private RelativeLayout menuLayout; // 菜单的布局
    private RelativeLayout menuTop;
    private boolean ismoving = false;
   
    @Override
    public void onCreate() {
        super.onCreate();
        // 加载辅助球布局
        ballView = View.inflate(this, R.layout.floatball, null);
        floatImage = (Button) ballView.findViewById(R.id.float_image);
        setUpFloatMenuView();
        createView();
    }
   
    /**
     * 窗口菜单初始化
     */
    private void setUpFloatMenuView() {
        menuView = View.inflate(this, R.layout.floatmenu, null);
        menuLayout = (RelativeLayout) menuView.findViewById(R.id.menu);
        menuTop = (RelativeLayout) menuView.findViewById(R.id.lay_main);
        menuLayout.setOnClickListener(this);
        menuTop.setOnClickListener(this);
    }
   
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.lay_main:
            Toast.makeText(getApplicationContext(), "111", Toast.LENGTH_SHORT)
                    .show();
            break;
   
        default:
            if (pop != null && pop.isShowing()) {
                pop.dismiss();
            }
            break;
        }
    }
   
    private void createView() {
        wm = (WindowManager) getSystemService("window");
        ballWmParams = new WindowManager.LayoutParams();
        ballWmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        ballWmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        ballWmParams.gravity = Gravity.LEFT | Gravity.TOP;
        ballWmParams.x = 0;
        ballWmParams.y = 0;
        ballWmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        ballWmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        ballWmParams.format = PixelFormat.RGBA_8888;
        // 添加显示
        wm.addView(ballView, ballWmParams);
        // 注册触摸事件监听
        floatImage.setOnTouchListener(new OnTouchListener() {
            public boolean onTouch(View v, MotionEvent event) {
                x = event.getRawX();
                y = event.getRawY();
                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    ismoving = false;
                    mTouchStartX = (int) event.getX();
                    mTouchStartY = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    ismoving = true;
                    updateViewPosition();
                    break;
                case MotionEvent.ACTION_UP:
                    mTouchStartX = mTouchStartY = 0;
                    break;
                }
                // 如果拖动则返回false,否则返回true
                if (ismoving == false) {
                    return false;
                } else {
                    return true;
                }
            }
   
        });
        // 注册点击事件监听
        floatImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 获取在xml中定义的大小
                DisplayMetrics dm = getResources().getDisplayMetrics();
                pop = new PopupWindow(menuView, dm.widthPixels, dm.heightPixels);
                pop.showAtLocation(ballView, Gravity.CENTER, 0, 0);
                pop.update();
            }
        });
    }
   
    /**
     * 更新view的显示位置
     */
    private void updateViewPosition() {
        ballWmParams.x = (int) (x - mTouchStartX);
        ballWmParams.y = (int) (y - mTouchStartY);
        wm.updateViewLayout(ballView, ballWmParams);
    }
   
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

同分类推荐文章

  1. 「置顶」我做了什么 (2026-05-05 12:13:28)
  2. 万字长文推演:手机不再从 App 开始,Agent OS 如何接管任务入口 (2026-04-28 14:57:22)
  3. Android Perfetto 系列 10 - Binder 调度与锁竞争 (2025-11-16 15:33:30)

查看更多 移动开发 文章 →

建议继续学习

  1. 情绪版(Mood board)操作流程的新思考 (累计阅读 41,753)
  2. 50个活力和动感的网页设计-颜色的灵感 (累计阅读 34,442)
  3. android 开发入门 (累计阅读 19,529)
  4. 30套最好的网站开发与设计中使用的小型图标 (累计阅读 12,529)
  5. Android 连接SSID隐藏网络以及 LEAP 认证的方法 (累计阅读 9,539)
  6. sns视觉设计分享 (累计阅读 9,523)
  7. 网站导航设计的6大分类 (累计阅读 9,298)
  8. 让安卓手机通过代理翻墙的方法 (累计阅读 9,116)
  9. 手机产品设计方向 (累计阅读 7,953)
  10. 实时监控Android设备网络封包 (累计阅读 6,558)