精华内容
下载资源
问答
  • 浏览器悬浮窗: 操作办法:

    浏览器内悬浮窗:


    操作办法:


    展开全文
  • 悬浮窗代码: <div id="img2" style="position:absolute; visibility:visible; left:261px; top:348px; z-index:200; text-align:right" onMouseOver=stop_helpor_net() onMouseOut=www_helpor_net()> &...
  • 悬浮窗这个东西 相信大家很多人都使用过,但是在小米的手机上,应该很多人的悬浮窗是无法使用的,因为小米默认是关闭这个悬浮窗权限的。但是uc往往能绕过小米这个悬浮窗权限控制。除此之外 剪切板...

    UC浏览器应该是android手机里 最流行的浏览器之一了,他们有一个功能 相信大家都体验过,就是如果你复制了什么文字,(在其他app中 复制也有这个效果!,所以能猜到肯定是监控了剪切板),就会弹出一个悬浮窗。

    悬浮窗这个东西 相信大家很多人都使用过,但是在小米的手机上,应该很多人的悬浮窗是无法使用的,因为小米默认是关闭这个悬浮窗权限的。但是uc往往能绕过小米这个悬浮窗权限控制。除此之外 剪切板在api 11以下

    和11以上都是不一样的实现。所以我们要完全复制uc浏览器的这个功能,我们主要需要解决2个问题:

    1.对剪切板 这个api 做版本兼容处理。

    2.如何绕过悬浮窗权限检查 去实现在小米等收紧悬浮窗权限的手机里依然正常显示悬浮窗。

    首先来看api兼容处理怎么做:

    在以往app开发的时候,我们基本上都会使用到一个本地物理缓存文件夹,这个里面存放着我们本app的缓存图片啊 之类的其他信息,但是考虑到用户手机容量有限,我们在相当多的时候在操作这个缓存路径的时候是会判断他的大小的,

    如果太大了,我们就删除一部分缓存。通常我们会这么做:

     1  /**
     2      * 返回path路径下的 所有文件大小
     3      * @param path 全路径
     4      * @return 返回-1代表path值为null
     5      */
     6     public static long getTotalSpace(File path)
     7     {
     8         if (path==null)
     9         {
    10             return -1;
    11         }
    12         return path.getTotalSpace();
    13     }

    看上去代码很完美对吧,但是如果我们改一个地方minSdkVersion 改成8

     1 android {
     2     compileSdkVersion 23
     3     buildToolsVersion "23.0.1"
     4 
     5     defaultConfig {
     6         applicationId "com.example.administrator.clipboardmanagertest"
     7         minSdkVersion 8
     8         targetSdkVersion 23
     9         versionCode 1
    10         versionName "1.0"
    11     }
    12     buildTypes {
    13         release {
    14             minifyEnabled false
    15             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    16         }
    17     }
    18 }

    我们再看:

    你看 ide直接报错了,原来这个api要求是

    public static final int GINGERBREAD = 9;




    也就是说,这个getTotalSpace这个函数 一定得在9或者9以上的手机里才能正常使用 在9之下的比如8 ,是没有这个api的。
    有些人为了懒,他就这么做了:
     1  /**
     2      * 返回path路径下的 所有文件大小
     3      * @param path 全路径
     4      * @return 返回-1代表path值为null
     5      */
     6     @TargetApi(Build.VERSION_CODES.GINGERBREAD)
     7     public static long getTotalSpace(File path)
     8     {
     9         if (path==null)
    10         {
    11             return -1;
    12         }
    13         return path.getTotalSpace();
    14     }

    加了一个注解,这样编译能通过了,但实际上这并没有什么卵用,因为这段代码只要在api小于9的手机里执行 依然会报错的。

    因为小于9的手机里 没有这个方法。所以这里要做一个简单的api兼容:

     1  @TargetApi(Build.VERSION_CODES.GINGERBREAD)
     2     public static long getTotalSpace(File path)
     3     {
     4         if (path==null)
     5         {
     6             return -1;
     7         }
     8         //如果这个sdk大于9 那就使用系统的api
     9         if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.GINGERBREAD)
    10         {
    11 
    12             return path.getTotalSpace();
    13         }else//小于9 系统没有这个api 我们就自己算吧。
    14         {
    15             final StatFs statFs=new StatFs(path.getPath());
    16             return statFs.getBlockSize()*statFs.getBlockCount();
    17         }
    18     }

    你看这样做就很完美了。同样的,我们在剪切板这个api 上也一样要做兼容处理:

    你想一下 uc的那个功能,其实肯定就是开启了一个服务,然后在服务里 监听剪切板的变化对吧,那就看看剪切板的变化 怎么监听:

     1  public void testCliboardApi()
     2     {
     3         ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
     4         clipboard.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {
     5             @Override
     6             public void onPrimaryClipChanged() {
     7 
     8             }
     9         });
    10     }

    实际上就是通过这个api进行监听剪切板的变化的,但是 这个api只能支持11或者11以上啊你改成10 就会报错了:

    所以我们的目标就是让这个api在11以下的版本也能兼容。好 现在就来完成这个功能,我们首先来自定义一个接口,这个接口实际上就只是写了5个方法 这5个方法在api 》11的 源码里面是都有实现的。

    在<11的源码里,实际上只有3个方法实现了,还有2个没有实现(我们主要就是要在小于api11的里面 实现这2个方法)

     

     1 package com.example.administrator.clipboardmanagertest;
     2 
     3 /**
     4  * Created by Administrator on 2015/11/25.
     5  */
     6 //这里我们就定义一个接口,这个接口囊括了 所有我们需要使用的方法
     7 //注意后三个方法 api11以下也是有的,而前2个方法 11或者11以上才有
     8 public interface ClipboardManagerInterfaceCompat {
     9 
    10     //注意这里的参数 我们使用的是自己定义的接口 而不是sdk里面的ClipboardManager.OnPrimaryClipChangedListener
    11     void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener);
    12 
    13     void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener);
    14 
    15     CharSequence getText();
    16 
    17     void setText(CharSequence text);
    18 
    19     boolean hasText();
    20 
    21 
    22 }

    然后我们可以看一下高于api11的版本里面,这个监听剪切板变化的功能是怎么做的,来稍微看一下源码:

    其实也很简单,无非就是发生内容变化的时候 回调一下这个接口的onPrimaryClipChanged方法罢了。

    为了兼容 我们也定义一个这样的接口,实际上就是把这段代码给抠出来。

     1 package com.example.administrator.clipboardmanagertest;
     2 
     3 /**
     4  * Created by Administrator on 2015/11/25.
     5  */
     6 
     7 //注意这个OnPrimaryClipChangedListener 是在api11以后才有的
     8 //我们这里就是把这个接口给拿出来 定义一下 看下CliboardManager的源码就知道了(注意要看api11 以后的源码)
     9 public interface OnPrimaryClipChangedListener {
    10     void onPrimaryClipChanged();
    11 }

    然后继续,我们可以想一下 既然是要对api11 以上和以下做2个版本,但实际上这2个版本 都得实现我们上面一开始的那个接口,所以可以定义一个抽象类 帮助我们完成这个功能:

     1 package com.example.administrator.clipboardmanagertest;
     2 
     3 import java.util.ArrayList;
     4 
     5 /**
     6  * Created by Administrator on 2015/11/25.
     7  */
     8 //既然我们是要对api11 以上和以下 分别做2个 实体类出来,而且这2个实体类 都必须实现我们的自定义接口。
     9 //所以不妨先定义一个base 的抽象类
    10 public  abstract class ClipboardManagerInterfaceCompatBase implements ClipboardManagerInterfaceCompat{
    11 
    12     //这个抽象类实际上就只做了一件事 维持一个监听器的list 罢了。
    13     //注意OnPrimaryClipChangedListener 这个类 是我们自定义的,不是高于api11的源码里的
    14     protected final ArrayList<OnPrimaryClipChangedListener> mPrimaryClipChangedListeners
    15             = new ArrayList<OnPrimaryClipChangedListener>();
    16 
    17     @Override
    18     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
    19         synchronized (mPrimaryClipChangedListeners) {
    20             mPrimaryClipChangedListeners.add(listener);
    21         }
    22     }
    23 
    24    //这个方法其实还挺重要的 就是通知所有在这个上面的listenser 内容发生了变化
    25     //注意这里的mPrimaryClipChangedListeners是自定义的 不是系统的
    26     protected final void notifyPrimaryClipChanged() {
    27         synchronized (mPrimaryClipChangedListeners) {
    28             for (int i = 0; i < mPrimaryClipChangedListeners.size(); i++) {
    29                 mPrimaryClipChangedListeners.get(i).onPrimaryClipChanged();
    30             }
    31         }
    32     }
    33 
    34     @Override
    35     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
    36         synchronized (mPrimaryClipChangedListeners) {
    37             mPrimaryClipChangedListeners.remove(listener);
    38         }
    39     }
    40 }

    好,抽象类也有了,我们就来写一下实体类,首先来实现一个高于api11的 类,这个比较简单:实际上就是引用原来系统的代码就可以了:

     1 package com.example.administrator.clipboardmanagertest;
     2 
     3 import android.annotation.TargetApi;
     4 import android.content.ClipboardManager;
     5 import android.content.Context;
     6 import android.os.Build;
     7 
     8 /**
     9  * Created by Administrator on 2015/11/25.
    10  */
    11 //注意这个实际上对应的就是api11 以上的ClipboardManager了,其实这个是最简单的,你只要调用系统的ClipboardManager 即可
    12 //不要遗漏注解 TargetApi 因为遗漏的话 编译会不过的
    13 public class ClipboardManagerInterfaceCompatImplNormal extends ClipboardManagerInterfaceCompatBase {
    14 
    15     ClipboardManager.OnPrimaryClipChangedListener mOnPrimaryClipChangedListener = new ClipboardManager.OnPrimaryClipChangedListener() {
    16         @Override
    17         public void onPrimaryClipChanged() {
    18             notifyPrimaryClipChanged();
    19         }
    20     };
    21     private ClipboardManager mClipboardManager;
    22 
    23     public ClipboardManagerInterfaceCompatImplNormal(Context context) {
    24         mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
    25     }
    26 
    27     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    28     @Override
    29     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
    30         super.addPrimaryClipChangedListener(listener);
    31         synchronized (mPrimaryClipChangedListeners) {
    32             if (mPrimaryClipChangedListeners.size() == 1) {
    33                 mClipboardManager.addPrimaryClipChangedListener(mOnPrimaryClipChangedListener);
    34             }
    35         }
    36     }
    37 
    38     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    39     @Override
    40     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
    41         super.removePrimaryClipChangedListener(listener);
    42         synchronized (mPrimaryClipChangedListeners) {
    43             if (mPrimaryClipChangedListeners.size() == 0) {
    44                 mClipboardManager.removePrimaryClipChangedListener(mOnPrimaryClipChangedListener);
    45             }
    46         }
    47     }
    48 
    49     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    50     @Override
    51     public CharSequence getText() {
    52         return mClipboardManager.getText();
    53     }
    54 
    55     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    56     @Override
    57     public void setText(CharSequence text) {
    58         if (mClipboardManager != null) {
    59             mClipboardManager.setText(text);
    60         }
    61     }
    62 
    63     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    64     @Override
    65     public boolean hasText() {
    66         return mClipboardManager != null && mClipboardManager.hasText();
    67     }
    68 
    69 }

    你看这个高于api11的 实体类 无非就是把api11的 给包了一层罢了。很简单。那我们来看看如何做api11 向下的兼容实体类。

      1 package com.example.administrator.clipboardmanagertest;
      2 
      3 import android.content.Context;
      4 import android.os.Handler;
      5 import android.os.Looper;
      6 import android.text.ClipboardManager;
      7 import android.text.TextUtils;
      8 import android.util.Log;
      9 
     10 import java.util.logging.LogRecord;
     11 
     12 /**
     13  * Created by Administrator on 2015/11/25.
     14  */
     15 //这个就是对应的api11 以下的ClipboardManager 实体类了,实际上这里主要就是要实现api11 以上的那个监听
     16 //我们就用一个最简单的方法 不断监视text变化就可以了
     17 //思路其实也挺简单的 就是把这个 ClipboardManagerInterfaceCompatImplCustom
     18 public class ClipboardManagerInterfaceCompatImplCustom extends ClipboardManagerInterfaceCompatBase implements Runnable {
     19 
     20     //静态的不会导致内存泄露
     21     private static Handler mHandler;
     22     private CharSequence mLastText;
     23     //这个是设置间隔多少毫秒去检查一次 默认我们设置成1000ms检查一次
     24     public static int CHECK_TIME_INTERVAL = 1000;
     25 
     26 
     27     static {
     28         mHandler = new Handler(Looper.getMainLooper());
     29     }
     30 
     31     //api11 以下 是android.text.ClipboardManager; 注意和api11以上的android.content.ClipboardManager是 有区别的
     32     ClipboardManager clipboardManager;
     33 
     34     public ClipboardManagerInterfaceCompatImplCustom(Context context) {
     35         clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
     36     }
     37 
     38 
     39     @Override
     40     public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
     41         super.addPrimaryClipChangedListener(listener);
     42         synchronized (mPrimaryClipChangedListeners) {
     43             if (mPrimaryClipChangedListeners.size() == 1) {
     44                 startListenDataChange();
     45             }
     46         }
     47     }
     48 
     49     @Override
     50     public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener listener) {
     51         super.removePrimaryClipChangedListener(listener);
     52         synchronized (mPrimaryClipChangedListeners) {
     53             if (mPrimaryClipChangedListeners.size() == 0) {
     54                 stopListenDataChange();
     55             }
     56         }
     57     }
     58 
     59     private void stopListenDataChange() {
     60         mHandler.removeCallbacks(this);
     61     }
     62 
     63     private void startListenDataChange() {
     64         mLastText = getText();
     65         mHandler.post(this);
     66     }
     67 
     68 
     69     @Override
     70     public CharSequence getText() {
     71 
     72         if (clipboardManager == null) {
     73             return null;
     74         }
     75 
     76         return clipboardManager.getText();
     77     }
     78 
     79     @Override
     80     public void setText(CharSequence text) {
     81         if (clipboardManager != null) {
     82             clipboardManager.setText(text);
     83         }
     84     }
     85 
     86     @Override
     87     public boolean hasText() {
     88         if (clipboardManager==null)
     89         {
     90             return false;
     91         }
     92         return clipboardManager.hasText();
     93     }
     94 
     95     @Override
     96     public void run() {
     97 
     98         CharSequence data=getText();
     99         isChanged(data);
    100         mHandler.postDelayed(this,CHECK_TIME_INTERVAL);
    101 
    102     }
    103 
    104     private void isChanged(CharSequence data)
    105     {
    106         if (TextUtils.isEmpty(mLastText) && TextUtils.isEmpty(data)) {
    107             return;
    108         }
    109         if (!TextUtils.isEmpty(mLastText) && data != null && mLastText.toString().equals(data.toString())) {
    110             return;
    111         }
    112         mLastText = data;
    113         //如果发生了变化 就通知
    114         notifyPrimaryClipChanged();
    115     }
    116 }

    最后定义一个util

     1 package com.example.administrator.clipboardmanagertest;
     2 
     3 import android.content.Context;
     4 import android.os.Build;
     5 
     6 /**
     7  * Created by Administrator on 2015/11/25.
     8  */
     9 public class CliboardManagerUtils {
    10     public static ClipboardManagerInterfaceCompat create(Context context) {
    11         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    12             return new ClipboardManagerInterfaceCompatImplNormal(context);
    13         } else {
    14             return new ClipboardManagerInterfaceCompatImplCustom(context);
    15         }
    16     }
    17 }

    然后,我们开启一个服务 来监听下 即可:

    package com.example.administrator.clipboardmanagertest;
    
    import android.app.Service;
    import android.content.Intent;
    import android.os.IBinder;
    import android.util.Log;
    import android.widget.Toast;
    
    public class MonitorService extends Service {
    
        private ClipboardManagerInterfaceCompat clipboardManagerInterfaceCompat;
    
        public MonitorService() {
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            // TODO: Return the communication channel to the service.
            throw new UnsupportedOperationException("Not yet implemented");
        }
    
        @Override
        public void onCreate() {
            clipboardManagerInterfaceCompat = CliboardManagerUtils.create(this);
            clipboardManagerInterfaceCompat.addPrimaryClipChangedListener(new OnPrimaryClipChangedListener() {
                @Override
                public void onPrimaryClipChanged() {
    
                    Toast.makeText(MonitorService.this, "监听到剪切板发生了变化", Toast.LENGTH_LONG).show();
                }
            });
            super.onCreate();
        }
    }

    最后我们来看一下效果,高于11的版本的效果我就不放了,因为是调用系统的所以肯定成功的,我们看看2.3这个低于11版本的效果 就好了:

     

    剪切的api兼容 我们做完了,那最后再看一下如何弹出悬浮窗 :

     

     1 package com.example.administrator.clipboardmanagertest;
     2 
     3 import android.app.Service;
     4 import android.content.Context;
     5 import android.content.Intent;
     6 import android.graphics.PixelFormat;
     7 import android.os.Build;
     8 import android.os.IBinder;
     9 import android.view.Gravity;
    10 import android.view.View;
    11 import android.view.WindowManager;
    12 import android.widget.TextView;
    13 
    14 public class MonitorService extends Service {
    15 
    16     private ClipboardManagerInterfaceCompat clipboardManagerInterfaceCompat;
    17 
    18     public MonitorService() {
    19     }
    20 
    21     @Override
    22     public IBinder onBind(Intent intent) {
    23         // TODO: Return the communication channel to the service.
    24         throw new UnsupportedOperationException("Not yet implemented");
    25     }
    26 
    27     @Override
    28     public void onCreate() {
    29         mWindowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
    30         clipboardManagerInterfaceCompat = CliboardManagerUtils.create(this);
    31         clipboardManagerInterfaceCompat.addPrimaryClipChangedListener(new OnPrimaryClipChangedListener() {
    32             @Override
    33             public void onPrimaryClipChanged() {
    34                 showText(clipboardManagerInterfaceCompat.getText().toString());
    35             }
    36         });
    37         super.onCreate();
    38     }
    39     private WindowManager mWindowManager;
    40 
    41     public void showText(String mContent)
    42     {
    43         final View rootView =  View.inflate(this, R.layout.content_view, null);
    44 
    45         rootView.setOnClickListener(new View.OnClickListener() {
    46 
    47             @Override
    48             public void onClick(View v) {
    49                 mWindowManager.removeView(rootView);
    50             }
    51         });
    52         final TextView mTextView;
    53         mTextView = (TextView) rootView.findViewById(R.id.contentTv);
    54         mTextView.setText(mContent);
    55 
    56         int w = WindowManager.LayoutParams.MATCH_PARENT;
    57         int h = WindowManager.LayoutParams.WRAP_CONTENT;
    58 
    59         int flags = 0;
    60         int type = 0;
    61         //api版本大于19的时候 TYPE_TOAST用这个参数 可以绕过绝大多数对悬浮窗权限的限制,比如miui
    62         //在小于19的时候 其实也是可以绕过的,只不过小于19你绕过了以后 点击事件就无效了 所以小于19的时候
    63         //还是用TYPE_PHONE
    64         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    65             type = WindowManager.LayoutParams.TYPE_TOAST;
    66         } else {
    67             type = WindowManager.LayoutParams.TYPE_PHONE;
    68         }
    69 
    70         WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(w, h, type, flags, PixelFormat.TRANSLUCENT);
    71         layoutParams.gravity = Gravity.TOP;
    72         mWindowManager.addView(rootView, layoutParams);
    73     }
    74 
    75 }

    别忘记权限:

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    最后看下效果


    到这里应该来说模仿的就差不多了,当然你要完全做的和UC一样还是要稍微润色一下ui的,此外,还要监听下启动手机时候的广播,当手机启动的时候 接收到广播
    就启动这个监听剪切板的服务即可。点击事件也要稍微修改一下,比如点击以后去你自己的业务逻辑activity 等等。

    转载于:https://www.cnblogs.com/punkisnotdead/p/4996683.html

    展开全文
  • Android越过悬浮窗权限显示悬浮窗

    千次阅读 2016-09-18 17:39:04
    文/Shawon(简书作者) 原地址 http://www.jianshu.com/p/167fd5f47d5c ,经验证,此方法可以越过大多数手机的悬浮球权限。 但是对MIUI8无效,MIUI8中应该是做了特殊处理。...最近UC浏览器中文版出

    文/Shawon(简书作者)

    原地址 http://www.jianshu.com/p/167fd5f47d5c ,经验证,此方法可以越过大多数手机的悬浮球权限。

    但是对MIUI8无效,MIUI8中应该是做了特殊处理。有待继续研究

    对应的Demo地址:https://github.com/pengjianbo/FloatViewFinal


    前言

    最近UC浏览器中文版出了一个快速搜索的功能, 在使用其他app的时候, 如果复制了一些内容, 屏幕顶部会弹一个窗口, 提示一些操作, 点击后跳转到UC, 显示这个悬浮窗不需要申请android.permission.SYSTEM_ALERT_WINDOW权限.

    如下图, 截图是在使用Chrome时截的, 但是屏幕顶部却有UC的view浮在屏幕上. 我使用的是小米, 我并没有给UC授悬浮窗权限, 所以我看到这个悬浮窗时是很震惊的.


    截图

    悬浮窗原理

    做过悬浮窗功能的人都知道, 要想显示悬浮窗, 要有一个服务运行在后台, 通过getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 然后向其中addView, addView第二个参数是一个WindowManager.LayoutParams, WindowManager.LayoutParams中有一个成员type, 有各种值, 一般设置成TYPE_PHONE就可以悬浮在很多view的上方了, 但是调用这个方法需要申请android.permission.SYSTEM_ALERT_WINDOW权限, 在很多机型上, 这个权限的名字叫悬浮窗, 比如小米手机上默认是禁用这个权限的, 有些恶意app会用这个权限弹广告, 而且很难追查是哪个应用弹的. 如果这个权限被禁用, 那么结果就是悬浮窗无法展示, 比如有道词典复制查词功能, 在小米手机上经常没用, 其实是用户没有授权, 而且应用也没有引导用户给它打开授权.

    现在UC能突破这个限制, 我很好奇它是怎么做到的.

    研究实现

    Android开发有点蛋疼的地方就是太容易被反编译, 但有时这也成为我们研究别人app的一种手段.

    反编译

    使用apktool可以很轻松的反编译UC.

    找代码

    逆向别人的app, 比较关键的地方是怎么找代码, 因为代码基本上都是混淆的, 直接看肯定是看不懂的, 只能去找, 突破口一般在字符资源上, 比如我们看到上图中的快速搜索是UC的字符, 那么我们到res/values/strings.xml去找快速搜索, 就可以找到下面的内容

    <string name="dark_search_banner_search">快速搜索</string>

    这里我们拿到了快速搜索对应的名字dark_search_banner_search, Android在编译时会给每个资源分配一个id, 我们grep一下这个字符资源的名字就能知道id是多少, 一般在R.java, res/values/public.xml中有定义, 我直接到public.xml中找到了它的id

    <public type="string" name="dark_search_banner_search" id="0x7f070049" />

    有了字符资源的id 0x7f070049, 我们再在代码里面grep一下这个id, 就能知道哪几个文件使用了这个字符资源.

    之所以这么确定是在代码里, 是因为UC在我们复制的内容不同时, 悬浮窗标题会不一样, 一定是在代码里控制的, 结果如下

    ./com/uc/browser/b/f.smali

    结果可能和大家不一样, 但是一定会找到一个被混淆的smali文件

    看代码

    这一部应该是最恶心的. smali代码和java代码的关系, 就像汇编代码和C++代码, 但是smali比汇编代码要容易理解的多, 不然也不会有那么多公司故意将代码写在C++层了.

    虽然代码都被混淆了, 而且以我们不熟悉的方式出现, 但我们可以根据一些蛛丝马迹来判断代码的执行, 比如Framework的类和API是不能被混淆的, 这也是我们能看懂smali的原因之一, 我们可以结合这些面包屑来还原整个app代码, 当然这需要我们对smali很熟悉, 如果不熟悉smali, 至少要对Android的API熟悉. 因为有时实在看不懂, 我们要靠猜来还原一段代码的逻辑.

    首先在代码里面找到0x7f070049, 发现了如下代码

        (省略)
        const v3, 0x7f070049
    
        invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
    
        move-result-object v1
    
        iput-object v1, v0, Lcom/uc/browser/b/a;->dpC:Ljava/lang/String;
    
        :cond_9
    
        (省略)
    
        invoke-virtual {v0, v1}, Lcom/uc/browser/b/a;->o(Landroid/graphics/drawable/Drawable;)V
        :try_end_2
        .catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_0
    
        goto/16 :goto_0
        (省略)

    这是0x7f070049出现之后的一部分代码, 一路看下来, 其实都是在取值赋值, 就拿0x7f070049来说:

    #使v3寄存器的值为0x7f070049
        const v3, 0x7f070049
    #v1是Resources实例, 调用它的getString方法, 方法的参数是v3中的值
        invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
    #将结果存入v1寄存器
        move-result-object v1

    其实就是我们常用的getResources().getString
    其实如果一直这么看下去, 会发现毫无头绪, 剩下的代码一直在干差不多的事情, 所以我只截取了这部分, 注意最后一行

    goto/16 :goto_0

    也就是说, 有可能代码转到goto_0那儿去了, 那么看看goto_0那里又写了些什么

        :goto_0
        (省略)
    
        const-string v1, "window"
    
        invoke-virtual {v0, v1}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
    
        move-result-object v0
    
        check-cast v0, Landroid/view/WindowManager;
    
        invoke-interface {v0}, Landroid/view/WindowManager;->getDefaultDisplay()Landroid/view/Display;
    
        move-result-object v0
    
        invoke-virtual {v0}, Landroid/view/Display;->getWidth()I
    
        move-result v0
    
        iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        iput v0, v1, Landroid/view/WindowManager$LayoutParams;->width:I
    
        iget-object v0, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        invoke-virtual {v10}, Lcom/uc/browser/b/a;->getContext()Landroid/content/Context;
    
        move-result-object v1
    
        invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
    
        move-result-object v1
    
        const v2, 0x7f0d0022
    
        invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getDimension(I)F
    
        move-result v1
    
        float-to-int v1, v1
    
        iput v1, v0, Landroid/view/WindowManager$LayoutParams;->height:I
    
        iget-object v0, v10, Lcom/uc/browser/b/a;->mWindowManager:Landroid/view/WindowManager;
    
        iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        invoke-interface {v0, v10, v1}, Landroid/view/WindowManager;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V

    其实看到const-string v1, "window", 我们就应该有所警惕了, 这可能是关键代码了. 为什么这么说? 因为悬浮窗的实现里面, 需要获取WindowManager, 从而需要调用Context.getSystemService(Context.WINDOW_SERVICE), 而官方文档写了Context.WINDOW_SERVICE就是常量window. 而后我们看到代码中构造了WindowManager.LayoutParams, 最终在addView时传入.

    看到这里, 我也觉得很奇怪, 我在悬浮窗原理中写的是我知道的实现悬浮窗的方法, UC的实现好像跟我调用的是相同的API, 也没看到反射之类可能展示奇技淫巧的代码, 为什么UC就可以不需要权限直接显示悬浮窗呢?

    猜测

    我认为addView的第二个参数WindowManager.LayoutParams可能是关键, 所以我需要知道UC是如何构造这个WindowManager.LayoutParams的.

    由于是系统的类, 无法混淆, 直接搜索LayoutParams就找到了下面的代码

    iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    这句话就是把v10的值赋给v1, v10com/uc/browser/b/a的成员dpx, 那么打开com/uc/browser/b/a.smali看看dpx到底是怎么构造的.

        (省略)
    
    .field dpx:Landroid/view/WindowManager$LayoutParams;
    
        (省略)
        .line 68
        new-instance v0, Landroid/view/WindowManager$LayoutParams;
    
        invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V
    
        iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        .line 69
        if-eqz p2, :cond_0
    
        .line 70
        iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        const/16 v1, 0x7d5
    
        iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I
    
        .line 74
        :goto_0
        iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        const/4 v1, 0x1
    
        iput v1, v0, Landroid/view/WindowManager$LayoutParams;->format:I
        (省略)

    这里的代码就很简单的, 我最先看的是下面这段

        const/16 v1, 0x7d5
    
        iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

    这两句代码就是把WindowManager.LayoutParams.type字段设成0x7d5, 官网上写了0x000007d5是WindowManager.LayoutParams.TYPE_TOAST的值.

    验证

    实际测试了一下, 将type设置成TYPE_TOAST果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW权限就能显示一个悬浮窗.

    之前我一直以为调用了系统WindowManager.addView需要android.permission.SYSTEM_ALERT_WINDOW权限, 但实际上调用这个方法是不需要权限的, 在Android源码中有这么一段

    public int checkAddPermission(WindowManager.LayoutParams attrs) {
        int type = attrs.type;
    
        if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
                || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
            return WindowManagerImpl.ADD_OKAY;
        }
        String permission = null;
        switch (type) {
            case TYPE_TOAST:
                // XXX right now the app process has complete control over
                // this...  should introduce a token to let the system
                // monitor/control what they are doing.
                break;
            case TYPE_INPUT_METHOD:
            case TYPE_WALLPAPER:
                // The window manager will check these.
                break;
            case TYPE_PHONE:
            case TYPE_PRIORITY_PHONE:
            case TYPE_SYSTEM_ALERT:
            case TYPE_SYSTEM_ERROR:
            case TYPE_SYSTEM_OVERLAY:
                permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
                break;
            default:
                permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
        }
        if (permission != null) {
            if (mContext.checkCallingOrSelfPermission(permission)
                    != PackageManager.PERMISSION_GRANTED) {
                return WindowManagerImpl.ADD_PERMISSION_DENIED;
            }
        }
        return WindowManagerImpl.ADD_OKAY;
    }

    可以猜到这个方法是往系统的WindowManageraddView的时候做权限检查用的, 那个type就是我们在构造WindowManager.LayoutParams时赋值的type, 可以看到, 除了TYPE_TOAST, 其他都是要权限的, 而且非常喜感的是, 代码中的注释还说他们现在对这种type毫无限制, 应该引入标记来限制开发者.

    处理兼容性

    在这篇文章刚刚公布的时候, 就有同学反馈悬浮窗无法接收事件, 刚开始我并没有特别在意, 在廖祜秋大神做了一个demo之后, 这篇文章阅读量又涨了不少, 随即收到更多反馈事件的问题, 我今天晚上借了台MIUI V5 4.2.2实测了一下, 这台机器上UC的快速搜索功能也无法正常使用.

    在这个ROM上表现为:
    使用TYPE_PHONE这类需要权限的type时, 只有在app处于前台时能显示悬浮窗, 且能正常接受触摸事件. 如果在应用详情里面授悬浮窗权限, 则工作完全正常.
    (这里是MIUI V5对悬浮窗的特殊处理, 现在的ROM, 包括MIUI V6上, 如果不授权, 无法显示任何悬浮窗)
    使用TYPE_TOAST这个不需要权限的type时, 悬浮窗正常显示, 但不能接受触摸事件.

    我重新检查了一下smali代码, 发现UC是有分版本处理的, 不过因为smali代码的规则问题, 很难直接看出来, 我把分析过程写出来, 顺便解释一下smali的语法, 供大家以后逆向时拿来参考.

    这次我是在OS X上反编译的, 所以变量名可能略有区别.

    接着上面com/uc/browser/b/a.smali中查看dpx的构造过程, 代码如下:

    .field dpx:Landroid/view/WindowManager$LayoutParams;
    
    (省略)
    
    # direct methods
    .method public constructor <init>(Landroid/content/Context;Z)V
        .locals 7
    
        (省略)
    
        .line 68
        new-instance v0, Landroid/view/WindowManager$LayoutParams;
    
        invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V
    
        iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        .line 69
        if-eqz p2, :cond_0
    
        .line 70
        iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        const/16 v1, 0x7d5
    
        iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

    为了方便说明, 我遵循smali的规则, 它用.line XX, 我们就说这是第XX行的代码.

    上面是我之前分析得到UC使用的是TYPE_TOAST的地方, 证据就是第70行的const/16 v1, 0x7d5, 但是要知道, smali代码没有跳转的话, 就是从上往下执行, 我们看第69行的代码如下:

    .line 69
    if-eqz p2, :cond_0

    这句话的意思是如果p2等于0, 控制流跳转到cond_0, 否则就是继续顺序往下执行. 也就是说UC只有在p2 != 0条件满足的时候才会使用TYPE_TOAST, 我们看看cond_0对应的代码.

        .line 72
        :cond_0
        iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
    
        const/16 v1, 0x7d2
    
        iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

    这里很简单, 就是将0x7d2赋给了type, 官网写了0x000007d2TYPE_PHONE, 也就是说UC在某种情况下还是会用需要权限的老方法展示悬浮窗.

    现在问题是条件是什么, 关键在p2, 在smali里面, 有两种寄存器命名规则, 一种叫v命名规则, 另一种是p命名规则, 当然只是命名规则而已, 在使用apktool时是可以选的. 这里是p命名规则.

    我刚才分析的赋值过程, 所在的方法是下面这个, 我在刚才的代码片段中也保留了这个部分.

    # direct methods
    .method public constructor <init>(Landroid/content/Context;Z)V
        .locals 7

    这就是com/uc/browser/b/a的构造方法, dpx就是在构造方法里初始化的, .locals 7告诉我们这个方法中将出现7个局部寄存器(local register), 名字是v0, v1...v6, 而这个方法的参数有3个, 隐式告诉我们这个方法中将出现3个参数寄存器(parameter register), 名字分别是p0, p1, p2.

    我是怎么知道这个方法有3个参数的呢. smali中非静态方法, 都隐含一个参数p0, 指向自身, 和Java中的this是一个意思, 而方法的参数写在括号里, 也就是Landroid/content/Context;Z, 其中Landroid/content/Context;很明显就是Android中的Context, 值存储在p1里, 而Z对应的是Android中的boolean, p2就是他了.

    也就是说, type是用TYPE_TOAST还是用TYPE_PHONE, 取决于这个构造方法的第二个参数, 那到底谁构造了com/uc/browser/b/a呢? 可以去代码里面搜形如new-instance ***, Lcom/uc/browser/b/a;的代码. 更保险的做法是搜Lcom/uc/browser/b/a然后一个一个的看.

    我在com/uc/browser/b/f.smali里面找到了下面的代码:

        .prologue
        const/4 v0, 0x0
    
        const/4 v1, 0x1
    
        (省略)
    
        new-instance v3, Lcom/uc/browser/b/a;
    
        iget-object v4, v9, Lcom/uc/browser/b/e;->mContext:Landroid/content/Context;
    
        sget v5, Landroid/os/Build$VERSION;->SDK_INT:I
    
        const/16 v6, 0x13
    
        if-lt v5, v6, :cond_0
    
        move v0, v1
    
        :cond_0
        invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

    这段代码首先是创建了com/uc/browser/b/a的实例, 存储在v3中, 从另一处拿到了一个Context存储在v4中, 然后拿到了当前系统的android.os.Build.VERSION.SDK_INT存储在v5中, 此时将v6的值设为0x13, 千万别粗心看成13了, 我好几次都觉得这是13, 其实是十进制的19, 接下来是一个条件分支, 如果v5的值小于v6, 也就是说android.os.Build.VERSION.SDK_INT < 19, 直接跳转到cond_0, 否则先将v1的值赋给v0, 再顺序执行.

    这句代码

    invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

    就是调用v3的构造方法, 参数是v4和v0, 分析一下上面这段代码的逻辑就是:
    如果当前系统API level小于19, 那么第二个参数就是0, 否则就是1.

    而这第二个参数的值就是之前我们分析的p2的值, UC只有在p2 != 0条件满足的时候才会使用TYPE_TOAST, 把整个逻辑串起来就是:

    UC在API level >= 19的时候, 使用TYPE_TOAST, 其他情况使用TYPE_PHONE(需要权限).

    可能是为了规避在低版本TYPE_TOAST不能接受事件的问题.

    关于针对源代码的分析, 请看Android悬浮窗使用TYPE_TOAST的小结

    实测效果

    我之前写的一个app有悬浮窗播放功能, 支持拖动窗口和点击暂停, 关闭窗口等等, 在4.4.4上实测功能正常.


    无权限悬浮窗演示gif

    感谢微博上关注的大神廖祜秋, 他做了个demo, 虽然交互和UC不同, 可以参考一下实现.


    廖祜秋大神的demo

    关于这个, 他也写了一篇Android 悬浮窗的小结

    其他补充

    评论区的浮海大虾同学有更多补充如下:

    TYPE_TOAST一直都可以显示, 但是用TYPE_TOAST显示出来的在2.3上无法接收点击事件, 因此还是无法随意使用.
    下面是我之前研究后台线程显示对话框的时候记得笔记, 大家可以看看我们项目中有需求需要在后台任务中显示Dialog, 项目最初的做法是用Activity模拟Dialog, 一个Activity已经承载了近20种Dialog, 代码混乱至极. 后来我发现Dialog可以通过改变Window Type实现不依赖Activity显示, 然后就很兴奋的要在使用这种方式来作为新的实现方式.
    最初WindowType是WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 可是这是悬浮窗了, MIUI会默认禁止(真他妈操蛋,也没有任何提示)最终放弃. 后来试着换成了WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI也不禁止了, 哪里都能显示, 这下开心了. 可是后来又发现在2.3上不能接收点击事件, 也就是说Dialog上的按钮不能点击, 这他妈就很操蛋了, 又放弃了. 又试了试其他的Type都不能满足需求, 结果如下:TYPE_SEARCH_BAR: 未知
    TYPE_ACCESSIBILITY_OVERLAY: 拒绝使用
    TYPE_APPLICATION: 只能配合Activity在当前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在当前APP使用
    TYPE_APPLICATION_MEDIA: 无法使用(什么也不显示)
    TYPE_APPLICATION_PANEL: 只能配合Activity在当前APP使用(PopupWindow默认就是这个Type)
    TYPE_APPLICATION_STARTING: 无法使用(什么也不显示)
    TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在当前APP使用TYPE_BASE_APPLICATION: 无法使用(什么也不显示)
    TYPE_CHANGED: 只能配合Activity在当前APP使用
    TYPE_INPUT_METHOD: 无法使用(直接崩溃)
    TYPE_INPUT_METHOD_DIALOG: 无法使用(直接崩溃)
    TYPE_KEYGUARD_DIALOG: 拒绝使用
    TYPE_PHONE: 属于悬浮窗(并且给一个Activity的话按下HOME键会出现看不到桌面上的图标异常情况)
    TYPE_TOAST: 不属于悬浮窗, 但有悬浮窗的功能, 缺点是在Android2.3上无法接收点击事件
    TYPE_SYSTEM_ALERT: 属于悬浮窗, 但是会被禁止

    尾声

    现在我们都知道了如何在不申请权限的情况下显示悬浮窗, 我相信以中国Android开发者的脑洞, 一定会有很多有趣或恶心的功能被开发出来, 一方面我自己觉得这个东西很有用, 可以实现一些很神奇的功能, 另一方面又担心这个API被滥用, 最终不得不限制权限.

    还有就是, 逆向分析仅用于学习, 不要干违法的事情.





    展开全文
  • 【推荐】js+DIV悬浮窗定位到鼠标位置的源代码(2013-09-04 22:34:33)标签:评论完美兼容火狐,IE等浏览器,js+DIV悬浮窗定位到鼠标位置的源代码!转帖请注明出处,这可是本站的原创内容,亦是本站使用的代码var oSon = ...

    【推荐】js+DIV悬浮窗定位到鼠标位置的源代码

    (2013-09-04 22:34:33)

    标签:

    评论

    完美兼容火狐,IE等浏览器,js+DIV悬浮窗定位到鼠标位置的源代码!转帖请注明出处,这可是本站的原创内容,亦是本站使用的代码

    var oSon = window.document.getElementByIdx_x(hint);

    //需要定位的层为:hint(div的id)

    with (oSon){

    style.display = block;//使层可见

    var e = event || window.event

    //获取鼠标位置

    if(typeof(e.pageX)!='undefined'){

    var x1 =e.pageX

    var y1 =

    e.pageY

    }else{

    var x1 =e.clientX

    var y1 =

    e.clientY

    }

    //获取滚动条位置

    var x = document.documentElement.scrollLeft ||

    document.body.scrollLeft

    var y = document.documentElement.scrollTop ||

    document.body.scrollTop

    //层的定位

    style.left=x+x1+'px';

    style.top=y+y1+'px';

    }

    原文:http://www.942v.com/c/1/9116.html

    分享:

    a4c26d1e5885305701be709a3d33442f.png喜欢

    0

    a4c26d1e5885305701be709a3d33442f.png赠金笔

    加载中,请稍候......

    评论加载中,请稍候...

    发评论

    登录名: 密码: 找回密码 注册记住登录状态

    昵   称:

    评论并转载此博文

    a4c26d1e5885305701be709a3d33442f.png

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

    展开全文
  • ios系统微信浏览器、safari浏览器中h5页面上拉下滑导致悬浮层脱离窗口的解决方法
  • js悬浮窗代码(支持拖动)</title> <meta name=description content=js浮动层特效制作悬浮在线客服代码,放置在线QQ等聊天按钮的在线客服浮动层代码,支持拖动效果,可随意在页面上拖动位置,随着浏览器滚动...
  • ios系统微信浏览器、safari浏览器中h5页面上拉下滑导致悬浮层脱离窗口的解决方法 一. 运行环境: iphone所有机型的qq浏览器,safari浏览器,微信内置浏览器(qq浏览器内核)等。 二. 异常现象: 1. 大幅度上下...
  • 需要jquery包 ... ;HEIGHT: 210px;WIDTH: 258px;...本视屏受理热线:88783838</div><a id="close" href="javascript:;...自动适应浏览器窗口大小定位 ie6 采用滚动条触发事件定位。 其他浏览器fixed 定位
  • ##ID悬浮窗消失解决方法 1.取消idm选项—常规—使用高级浏览器集成 的那一项,保存彻底退出idm; 2.关闭浏览器中的IDM扩展程序,以谷歌浏览器为例 3.删除C:\Users\jyp\AppData\Roaming\IDM\ 目录下的全部文件,重启...
  • 我怀疑是不是层级问题 ,直接在浏览器上对各个悬浮窗的样式设置了层级,也没效果… 查看文档好像没有提到这种场景.网上好像也没人遇到这个问题…于是我尝试给表格列的render函数props对象添加一个transfer:true,问题...
  • Android悬浮窗TYPE_TOAST小结源码分析

    千次阅读 2016-02-01 19:00:33
    Android无需权限显示悬浮窗, 兼谈逆向分析app这篇文章阅读量很大, 这篇文章是通过逆向分析UC浏览器的实现和兼容性处理来得到一个悬浮窗的实现小技巧, 但有很多问题没有弄明白, 比如为什么在API 18及以下TYPE_TOAST的...
  • web前端模仿微信悬浮窗效果

    千次阅读 2018-08-09 15:26:10
    微信新出了个悬浮窗的功能,因为业务需要,我用js写了个h5版本的,依赖jq或者zepto,可以自己选择改造、 请用手机或者电脑浏览器模拟手机模式查看 在线预览 代码如下 &lt;!doctype html&gt; &lt;...
  • Android悬浮窗TYPE_TOAST小结: 源码分析

    千次阅读 2016-10-22 15:40:42
    Android无需权限显示悬浮窗, 兼谈逆向分析app这篇文章阅读量很大, 这篇文章是通过逆向分析UC浏览器的实现和兼容性处理来得到一个悬浮窗的实现小技巧, 但有很多问题没有弄明白, 比如为什么在API 18及以下TYPE_TOAST的...
  • 最近UC浏览器中文版出了一个快速搜索的功能, 在使用其他app的时候, 如果复制了一些内容, 屏幕顶部会弹一个窗口, 提示一些操作, 点击后跳转到UC, 显示这个悬浮窗不需要申请android.permission.SYSTEM_ALERT_WINDOW...
  • 很多时候,我们需要设置一个dom节点到浏览器窗口的右下角。我们需要那个元素可以在窗口Scroll滚动或者变换大小resize的时候都可以保持浮动在那个位置。这个时候,我在网上看了看,发现很多框架什么啊,或者什么实例...
  • 如果一个页面有个浮动的二维码,当页面窗口缩小时二维码会遮盖住页面内容,这时候可以根据浏览器大小来决定显示方式: 1.当页面宽度足够大时,完全显示二维码, 2.当页面窗口缩小时,这时候需要显示一个按钮,...
  • 使用qml编写桌面悬浮窗

    千次阅读 2018-07-29 23:42:35
    跨平台的桌面桌面应用有Qt、Electron等,但是Electron这些的视觉效果感觉不太好,网页质感,而且安装包大(Chromium这浏览器来展示),使用qt的话,其他质感强一点和安装包小点。而qt中,对于qWidget和qml,qml没用...

空空如也

空空如也

1 2 3 4 5 ... 10
收藏数 197
精华内容 78
关键字:

悬浮窗浏览器