上次我们实现了对粘贴板的内容获取与复制行为的监听,这次我会带着大家完成悬浮窗功能.
创建悬浮窗的布局
首先我们先来点简单的,就是创建我们悬浮窗的布局文件.项目中的悬浮窗比较简单,是一个位于屏幕上方简洁的卡片,其布局文件如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00000000">
    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardBackgroundColor="#f2f4f6"
        app:cardCornerRadius="2dp"
        app:contentPaddingLeft="10dp"
        app:contentPaddingRight="10dp"
        app:contentPaddingBottom="10dp"
        app:contentPaddingTop="5dp">
        <RelativeLayout
            android:id="@+id/pv_rl_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <TextView
                android:id="@+id/pv_tv_query"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="3dp"
                android:ellipsize="end"
                android:maxLines="1"
                android:text="复制的内容"
                android:textColor="@android:color/black"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/pv_tv_translate"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/pv_tv_query"
                android:ellipsize="end"
                android:lineSpacingExtra="2dp"
                android:text="翻译的结果"
                android:maxLines="4"
                android:textColor="#aa000000" />
            <TextView
                android:text="词典的详细解析"
                android:id="@+id/pv_tv_explains"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/pv_tv_translate"
                android:ellipsize="end"
                android:lineSpacingExtra="2dp"
                android:maxLines="4"
                android:textColor="#aa000000" />
        </RelativeLayout>
    </android.support.v7.widget.CardView>
</LinearLayout>
布局的效果如下: 
  
悬浮窗的实现
其实悬浮窗的实现只需要通过系统的WindowManager向当前的界面添加一个View即可,所添加的View便是我们的悬浮窗.所有我们先来创建我们的View,新建一个名为PopView的类.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90public class PopView implements View.OnClickListener{
 private WindowManager mWindowManager;
    private Context mContext;
    // 取消悬浮窗的监听器
    private OnViewDismissListener mOnViewDismissListener;
    // 悬浮窗的View
    private LinearLayout mPopView;
    // 悬浮窗内的各个控件
    (R.id.pv_tv_query)
    TextView tv_query;
    (R.id.pv_tv_translate)
    TextView tv_translation;
    (R.id.pv_tv_explains)
    TextView tv_explain;
    // 作为悬浮窗是否显示的标志
    private boolean isDismiss = true;
    public PopView(Context application) {
        mContext = application;
        // 获取系统的WindowManager
        mWindowManager = (WindowManager) application.getSystemService(Context.WINDOW_SERVICE);
    }
    /**
     * 计算获取悬浮窗的布局参数
     * @return the popView layout parameter
     */
    private WindowManager.LayoutParams getPopViewParams() {
        int w = WindowManager.LayoutParams.MATCH_PARENT;
        int h = WindowManager.LayoutParams.WRAP_CONTENT;
        int flags = 0;
        int type;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            type = WindowManager.LayoutParams.TYPE_TOAST;
        } else {
            type = WindowManager.LayoutParams.TYPE_PHONE;
        }
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(w, h, type, flags, PixelFormat.TRANSLUCENT);
        layoutParams.gravity = Gravity.TOP;
        layoutParams.format = PixelFormat.RGBA_8888;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        layoutParams.gravity = Gravity.CENTER | Gravity.TOP;
        layoutParams.x = 0;
        layoutParams.y = 0;
        return layoutParams;
    }
    /**
     * remove the popView
     */
    private void removePoppedView() {
        if (isDismiss)return;
        if (mWindowManager != null && mPopView != null) {
            mWindowManager.removeView(mPopView);
        }
        isDismiss = true;
    }
    /**
     * remove the popView and handle the dismiss mission
     */
    private void removePoppedViewAndClear() {
        removePoppedView();
        if (mOnViewDismissListener != null) {
            mOnViewDismissListener.onViewDismiss();
        }
    }
    
    
    public void onClick(View view) {
        removePoppedViewAndClear();
    }
    public void setOnViewDismissListener(OnViewDismissListener listener) {
        mOnViewDismissListener = listener;
    }
    /**
     * dismiss listener interface
     */
    public interface OnViewDismissListener {
        void onViewDismiss();
    }
}
从上面的代码可以看出,创建悬浮窗需要以下的几个步骤:
- 获取系统的WindowManager对象
- 为悬浮窗设置相应的布局参数
- 展现悬浮窗及处理相应的事件
其中第2步就是我们代码里的getPopViewParams()方法,首先我们为悬浮窗设置了其宽高1
2int w = WindowManager.LayoutParams.MATCH_PARENT;
int h = WindowManager.LayoutParams.WRAP_CONTENT;
随后根据系统的版本来设置悬浮窗的类型,关于这部分能内容,有兴趣的可以看一下Android 悬浮窗的小结这篇博客,里面讲的很详细.1
2
3
4
5
6int type;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
    type = WindowManager.LayoutParams.TYPE_PHONE;
}
最后设置好其他的属性并把布局参数返回1
2
3
4
5
6
7
8
9int flags = 0;
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(w, h, type, flags, PixelFormat.TRANSLUCENT);
layoutParams.gravity = Gravity.TOP;
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.gravity = Gravity.CENTER | Gravity.TOP;
layoutParams.x = 0;
layoutParams.y = 0;
return layoutParams;
第3步是展示我们的悬浮窗,由于项目采用了MVP的架构,而PopView则是其中的一个View模块,下面我先给出有关悬浮窗的MVP代码PopContract.java1
2
3
4
5
6
7
8
9
10
11
12/**
 * the popup contract class
 */
public class PopContract {
    interface View extends BaseView<Presenter>{
        void showPopView(TransInfo info);
    }
    interface Presenter extends BasePresenter{
        void translate(String query);
    }
}
我们让PopView实现PopContract.View接口,然后实现showPopView()方法1
2
3
4
5
6
7
8
9
10
11
    public void showPopView(TransInfo info) {
        removePoppedView();
        if (mPopView == null) {
            mPopView = (LinearLayout) View.inflate(mContext, R.layout.pop_view, null);
            ButterKnife.bind(this, mPopView);
            mPopView.setOnClickListener(this);
        }
        mWindowManager.addView(mPopView, getPopViewParams());
        isDismiss = false;
    }
其中的TranInfo对象是我们获取网络翻译处理后的对象,我们会在后面讲到的.到这里我们的悬浮窗功能基本上实现了,但需要注意的是,由于我们用到了第三方的开源库,所以需要在依赖配置中配置我们的依赖app/build.gradle1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
    compileSdkVersion 23
    buildToolsVersion "24.0.0 rc3"
    defaultConfig {
        applicationId "com.source.kevin.fingertrans"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        testCompile 'junit:junit:4.12'
        compile 'com.android.support:appcompat-v7:23.4.0'
        compile 'com.android.support:design:23.4.0'
        compile 'com.jakewharton:butterknife:8.0.1'
        apt 'com.jakewharton:butterknife-compiler:8.0.1'
        compile 'com.android.support:cardview-v7:23.4.0'
}
项目根目录下的build.gradle文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.0.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
allprojects {
    repositories {
        jcenter()
    }
}
repositories{
    mavenLocal()
    mavenCentral()
}
task clean(type: Delete) {
    delete rootProject.buildDir
}
最后我们可以在MainActivity.java中添加我们的测试代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainActivity extends AppCompatActivity implements ClipboardManager.OnPrimaryClipChangedListener {
    Clipboard clipboard;
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        clipboard = Clipboard.getInstance();
        clipboard.setPrimaryClipChangedListener(this);
    }
    
    public void onPrimaryClipChanged() {
        String text = clipboard.getText();
        Log.e("Text", "粘贴板的内容是:" + text);
        PopView popView = new PopView(FingerApp.get());
        popView.showPopView(null);
    }  
}
记住我们还需要在AndroidManifest.xml文件添加悬浮窗权限1
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
运行我们的代码,当我们进行复制的时候就会在屏幕上方弹出我们的悬浮窗.可能部分的机型需要手动添加悬浮窗的权限. 
  
到这里我们悬浮窗的功能就基本上完成了,下次我会带大家完成数据部分的功能.