From 449bdb5d2b5bf7b272ca5cda4c066f9a65040064 Mon Sep 17 00:00:00 2001
From: lmw <125975490@qq.com>
Date: 星期二, 04 三月 2025 14:30:40 +0800
Subject: [PATCH] fix

---
 easyfloat/src/main/java/com/lzf/easyfloat/widget/BaseSwitchView.kt            |   21 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/FloatCallbacks.kt        |   58 
 easyfloat/src/main/java/com/lzf/easyfloat/utils/DefaultDisplayHeight.kt       |   15 
 easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_selected.png              |    0 
 easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/RomUtils.kt          |   77 +
 easyfloat/src/main/java/com/lzf/easyfloat/utils/LifecycleUtils.kt             |  106 +
 easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatMessage.kt                 |   14 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnPermissionResult.kt    |   11 
 easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MiuiUtils.java       |  161 ++
 easyfloat/src/main/java/com/lzf/easyfloat/anim/AnimatorManager.kt             |   25 
 easyfloat/src/main/java/com/lzf/easyfloat/utils/Logger.kt                     |   48 
 easyfloat/src/main/res/values/styles.xml                                      |   13 
 .idea/gradle.xml                                                              |    1 
 easyfloat/src/main/java/com/lzf/easyfloat/utils/DragUtils.kt                  |  177 ++
 easyfloat/src/main/res/values/strings.xml                                     |    5 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatAnimator.kt       |   29 
 app/build.gradle                                                              |    4 
 easyfloat/src/test/java/com/lzf/easyfloat/ExampleUnitTest.java                |   17 
 easyfloat/build.gradle                                                        |   37 
 easyfloat/src/main/java/com/lzf/easyfloat/utils/DisplayUtils.kt               |  172 ++
 .idea/jarRepositories.xml                                                     |    5 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatTouchListener.kt  |   13 
 easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatInitializer.kt             |   43 
 easyfloat/src/main/java/com/lzf/easyfloat/WindowEvent.kt                      |    3 
 app/src/main/java/com/sinata/xqmuse/utils/Const.kt                            |    7 
 easyfloat/src/main/java/com/lzf/easyfloat/EasyFloat.kt                        |  379 +++++
 easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_normal.png                |    0 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatCallbacks.kt      |   43 
 easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionFragment.kt    |   54 
 app/src/main/java/com/sinata/xqmuse/network/Apis.kt                           |    2 
 easyfloat/src/main/res/layout/window_dialog_common.xml                        |  112 +
 easyfloat/src/main/java/com/lzf/easyfloat/WindowDIalog.kt                     |   76 +
 easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowHelper.kt        |  337 ++++
 easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultCloseView.kt          |  154 ++
 easyfloat/src/main/java/com/lzf/easyfloat/widget/ParentFrameLayout.kt         |   72 +
 easyfloat/src/main/java/com/lzf/easyfloat/anim/DefaultAnimator.kt             |  146 ++
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnTouchRangeListener.kt  |   23 
 easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultAddView.kt            |   85 +
 easyfloat/proguard-rules.pro                                                  |   37 
 easyfloat/src/main/java/com/lzf/easyfloat/utils/InputMethodUtils.kt           |   60 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnInvokeView.java        |   18 
 easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowManager.kt       |   72 +
 app/src/main/java/com/sinata/xqmuse/ui/home/HomeFragment.kt                   |   11 
 easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/QikuUtils.java       |   78 +
 app/src/main/java/com/sinata/xqmuse/MainActivity.kt                           |  173 +
 app/src/main/java/com/sinata/xqmuse/ThinkAudioService.kt                      |  126 +
 easyfloat/src/main/java/com/lzf/easyfloat/enums/ShowPattern.kt                |   12 
 easyfloat/src/main/res/layout/default_close_layout.xml                        |   33 
 easyfloat/src/main/res/values/color.xml                                       |    4 
 easyfloat/src/main/res/drawable/bg_yellow_22dp.xml                            |    5 
 easyfloat/src/main/java/com/lzf/easyfloat/core/TouchUtils.kt                  |  334 ++++
 easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/OppoUtils.java       |   72 +
 easyfloat/src/main/res/layout/default_add_layout.xml                          |   30 
 easyfloat/src/main/java/com/lzf/easyfloat/enums/SidePattern.kt                |   20 
 easyfloat/src/main/res/drawable/base_rauis_for_white.xml                      |    6 
 easyfloat/src/main/res/mipmap-xxhdpi/ic_dialog_close.png                      |    0 
 easyfloat/src/main/java/com/lzf/easyfloat/BaseEventWindow.kt                  |    4 
 easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/HuaweiUtils.java     |  103 +
 easyfloat/src/androidTest/java/com/lzf/easyfloat/ExampleInstrumentedTest.java |   26 
 easyfloat/src/main/res/drawable/icon_delete_selected.png                      |    0 
 easyfloat/.gitignore                                                          |    1 
 easyfloat/src/main/res/mipmap-xxhdpi/icon_fuchuang.png                        |    0 
 settings.gradle                                                               |    1 
 app/src/main/java/com/sinata/xqmuse/ui/home/VoiceDetailActivity.kt            |   53 
 easyfloat/src/main/res/drawable/add_selected.xml                              |   25 
 app/src/main/AndroidManifest.xml                                              |   12 
 easyfloat/src/main/res/drawable/add_normal.xml                                |   25 
 easyfloat/src/main/res/drawable/icon_delete_normal.png                        |    0 
 easyfloat/src/main/java/com/lzf/easyfloat/data/FloatConfig.kt                 |   80 +
 easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MeizuUtils.java      |   72 +
 easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionUtils.kt       |  117 +
 easyfloat/src/main/AndroidManifest.xml                                        |   12 
 app/src/main/res/layout/layout_floter.xml                                     |   12 
 easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnDisplayHeight.java     |   21 
 easyfloat/src/main/res/values/attrs.xml                                       |   52 
 75 files changed, 4,129 insertions(+), 123 deletions(-)

diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 1102b1f..ee4c618 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -15,6 +15,7 @@
             <option value="$PROJECT_DIR$/dkplayer-java" />
             <option value="$PROJECT_DIR$/dkplayer-players" />
             <option value="$PROJECT_DIR$/dkplayer-players/exo" />
+            <option value="$PROJECT_DIR$/easyfloat" />
             <option value="$PROJECT_DIR$/imagepicker" />
             <option value="$PROJECT_DIR$/umeng_sdk" />
             <option value="$PROJECT_DIR$/xldutils-kotlin" />
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index eb2873e..1e2d92c 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -26,5 +26,10 @@
       <option name="name" value="Google" />
       <option name="url" value="https://dl.google.com/dl/android/maven2/" />
     </remote-repository>
+    <remote-repository>
+      <option name="id" value="MavenRepo" />
+      <option name="name" value="MavenRepo" />
+      <option name="url" value="https://repo.maven.apache.org/maven2/" />
+    </remote-repository>
   </component>
 </project>
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 80663ef..3e6dbab 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,7 +8,7 @@
 
     defaultConfig {
         applicationId "com.sinata.xqmuse"
-        minSdkVersion 21
+        minSdkVersion 23
         targetSdkVersion 30
         versionCode 10
         versionName "1.81"
@@ -131,4 +131,6 @@
     api "com.google.android.exoplayer:exoplayer-hls:2.19.1"
     api "com.google.android.exoplayer:exoplayer-rtsp:2.19.1"
     api "com.google.android.exoplayer:extension-rtmp:2.19.1"
+
+    implementation project(path: ':easyfloat')
 }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 86afd5c..0fb7ddd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,7 +23,11 @@
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
     <uses-permission android:name="android.permission.CAMERA" />
-
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 用于开启 debug 版本的应用在 6.0 系统上的层叠窗口权限 -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <!-- 音频播放权限声明 -->
+    <uses-permission android:name="android.permission.AUDIO_PLAYBACK" />
 
     <queries>
         <package android:name="com.eg.android.AlipayGphone" />
@@ -148,10 +152,16 @@
         <activity android:name=".ui.home.MicroVideoActivity" android:launchMode="singleTask"  android:configChanges="orientation|screenSize|keyboardHidden" />
 
         <service android:name="com.amap.api.location.APSService"/>
+        <service android:name=".ThinkAudioService"
+            android:enabled="true"
+            android:exported="false"
+            android:foregroundServiceType="mediaPlayback"/>
 
         <meta-data
             android:name="com.amap.api.v2.apikey"
             android:value="bd5f6ea87e4fcfa83804672c547c6b28"/>
+
+
     </application>
 
 </manifest>
\ No newline at end of file
diff --git a/app/src/main/java/com/sinata/xqmuse/MainActivity.kt b/app/src/main/java/com/sinata/xqmuse/MainActivity.kt
index c2fb678..0c67e6f 100644
--- a/app/src/main/java/com/sinata/xqmuse/MainActivity.kt
+++ b/app/src/main/java/com/sinata/xqmuse/MainActivity.kt
@@ -2,13 +2,12 @@
 
 import android.annotation.SuppressLint
 import android.content.Intent
-import android.os.Handler
-import android.os.Looper
-import android.os.Message
+import android.net.Uri
+import android.os.*
 import android.provider.Settings
 import android.util.Log
+import android.view.Gravity
 import android.view.View
-import android.view.WindowManager
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentPagerAdapter
 import cn.sinata.xldutils.gone
@@ -17,6 +16,10 @@
 import com.flyco.tablayout.listener.CustomTabEntity
 import com.flyco.tablayout.listener.OnTabSelectListener
 import com.google.gson.Gson
+import com.lzf.easyfloat.EasyFloat
+import com.lzf.easyfloat.enums.ShowPattern
+import com.lzf.easyfloat.enums.SidePattern
+import com.lzf.easyfloat.interfaces.OnInvokeView
 import com.sinata.xqmuse.dialog.TipDialog
 import com.sinata.xqmuse.network.HttpManager
 import com.sinata.xqmuse.network.entity.VoiceDetail
@@ -35,12 +38,14 @@
 import com.sinata.xqmuse.utils.Const
 import com.sinata.xqmuse.utils.event.EmptyEvent
 import com.sinata.xqmuse.utils.event.IntEvent
+import com.umeng.socialize.utils.DeviceConfigInternal.context
 import kotlinx.android.synthetic.main.activity_main.*
 import org.greenrobot.eventbus.EventBus
 import org.greenrobot.eventbus.Subscribe
 import org.jetbrains.anko.startActivity
 import org.jetbrains.anko.toast
 import xyz.doikki.videoplayer.player.VideoView
+
 
 class MainActivity : TransparentStatusBarActivity(), OnTabSelectListener,AudioUtils.OnAudioStatusUpdateListener {
     override fun setContentView() = R.layout.activity_main
@@ -49,7 +54,7 @@
 
     var teacherVideoView:VideoView? = null
     private var bgPlayer:AudioUtils? = null//背景音乐播放器
-    private var thinkBgPlayer:AudioUtils? = null//冥想背景音播放器
+//    private var thinkBgPlayer:AudioUtils? = null//冥想背景音播放器
 //    private var thinkPlayer:AudioUtils? = null//冥想背景音播放器
 
     private var guideAudio:String? = null
@@ -71,6 +76,9 @@
     var hasTreeFirstShow = false //此字段用来判断 树苗的首次弹窗是否已经触发,和isFirst字段配合使用
     var isBGMChanged = false //此字段用来判断 在疗愈播放中,修改了背景音乐,在疗愈结束后 需要更新BGM音源
 
+    private val EasyFloatTag = "BACKGROUND"
+    private var floater: EasyFloat.Builder? = null //浮窗
+
     override fun initClick() {
         player_close.setOnClickListener {
             TipDialog.show(supportFragmentManager, "是否关闭当前音频?", object : TipDialog.OnClickCallback {
@@ -84,20 +92,24 @@
         }
 
         cl_player.setOnClickListener {
-            voice?.goDetail(this)
+            ThinkAudioService.voice?.goDetail(this)
         }
 
         player_play.setOnClickListener {
-            playing = !playing
-            if (playing){
+            ThinkAudioService.playing = !ThinkAudioService.playing
+            if (ThinkAudioService.playing){
                 player_play.setImageResource(R.mipmap.player_pause)
-                thinkBgPlayer?.resume()
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.USER_INFO_CHANGED))
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.SERVICE_AUDIO_RESUME))
+//                thinkBgPlayer?.resume()
 //                thinkPlayer?.resume()
                 thinkHandler?.sendEmptyMessage(MSG_PROGRESS)
                 startTime = System.currentTimeMillis()
             }else{
                 player_play.setImageResource(R.mipmap.player_start)
-                thinkBgPlayer?.pause()
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.USER_INFO_CHANGED))
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.SERVICE_AUDIO_PAUSE))
+//                thinkBgPlayer?.pause()
 //                thinkPlayer?.pause()
                 thinkHandler?.removeMessages(MSG_PROGRESS)
                 saveThinkRecord()
@@ -123,17 +135,16 @@
                 super.handleMessage(msg)
                 when(msg.what){
                     MSG_PROGRESS -> {
-                        currentPosition = thinkBgPlayer?.currentPosition ?: 0
-                        EventBus.getDefault().post(EmptyEvent(Const.EventCode.GOT_THINK_POSITION))
+                        EventBus.getDefault().post(EmptyEvent(Const.EventCode.SERVICE_AUDIO_PROGRESS))
                         sendEmptyMessageDelayed(MSG_PROGRESS, 1000)
                     }
                     MSG_COUNTDOWN -> {
-                        if (System.currentTimeMillis() >= finishTime)
+                        if (System.currentTimeMillis() >= ThinkAudioService.finishTime)
                             EventBus.getDefault().post(EmptyEvent(Const.EventCode.FINISH_THINK))
                         else
                             sendEmptyMessageDelayed(MSG_COUNTDOWN, 1000)
                     }
-                    MSG_TODAY -> {
+                    MSG_TODAY -> { //todo 分离hanlder,这个保持原功能,进度和计时相关应放进Service
                         if (System.currentTimeMillis() - lastTodayTime > 60000) { //距离上次刷新过去了1分钟
                             Log.e(Const.Tag, "已经过1分钟,需要重新获取今日疗愈数据")
                             lastTodayTime = System.currentTimeMillis()
@@ -183,21 +194,22 @@
      */
     private fun startThink() {
         bgPlayer?.pause()
-        index = 0
-        if (voice?.meditationMusicList?.isNullOrEmpty() == false){
-            if (thinkBgPlayer == null){
-                thinkBgPlayer = AudioUtils()
-                thinkBgPlayer!!.setOnAudioStatusUpdateListener(this)
+        ThinkAudioService.index = 0
+        if (ThinkAudioService.voice?.meditationMusicList?.isNullOrEmpty() == false){
+            checkFloat() //检测浮窗
+            // 启动音乐服务
+            val serviceIntent = Intent(this, ThinkAudioService::class.java)
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                startForegroundService(serviceIntent)
+            } else {
+                startService(serviceIntent)
             }
-            val volume = SPUtils.instance().getInt(Const.User.VOLUME_THINK, 50)
-            thinkBgPlayer?.setVolume(volume.toFloat() / 100)
-            thinkBgPlayer?.startPlayMusic(this, voice?.meditationMusicList?.get(index))
-            currentDuration = voice?.meditationSecondList?.get(index)?:0
+            ThinkAudioService.currentDuration = ThinkAudioService.voice?.meditationSecondList?.get(ThinkAudioService.index)?:0
             EventBus.getDefault().post(EmptyEvent(Const.EventCode.GOT_THINK_DURATION))
-            playing = true
+            ThinkAudioService.playing = true
             cl_player.visible()
-            tv_player.text = voice?.meditationTitle
-            iv_player.setImageURI(voice?.coverUrl?.split(",")?.firstOrNull())
+            tv_player.text = ThinkAudioService.voice?.meditationTitle
+            iv_player.setImageURI(ThinkAudioService.voice?.coverUrl?.split(",")?.firstOrNull())
             player_play.setImageResource(R.mipmap.player_pause)
             thinkHandler?.sendEmptyMessage(MSG_PROGRESS)
             startTime = System.currentTimeMillis()  //记录开始冥想的时间
@@ -210,11 +222,12 @@
      */
     private fun finishThink(){
         saveThinkRecord()
-        voice = null
-        index = 0
-        finishTime = 0L
-        thinkBgPlayer?.stopPlayMusic(true)
-        playing = false
+        ThinkAudioService.voice = null
+        ThinkAudioService.index = 0
+        ThinkAudioService.finishTime = 0L
+        // 停止服务
+        stopService(Intent(this, ThinkAudioService::class.java))
+        ThinkAudioService.playing = false
         thinkHandler?.removeMessages(0)
         cl_player.gone()
         (fragments[0] as HomeFragment).refreshTodayPlayingState() //对比当前音频是否是每日疗愈
@@ -227,14 +240,41 @@
     }
 
     /**
+     * 申请浮窗权限 增加稳定性
+     */
+    private fun checkFloat() {
+        if (!Settings.canDrawOverlays(this) && SPUtils.instance().getString("isRefusedFloat").isNullOrEmpty()) { //没有浮窗权限并且没有拒绝过
+            TipDialog.show(
+                supportFragmentManager,
+                "为了增加后台播放的稳定性,我们需要开启悬浮窗口权限",
+                object : TipDialog.OnClickCallback {
+                    override fun onOk() {
+                        var intent = Intent(
+                            Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+                            Uri.parse("package:" + packageName)
+                        )
+                        startActivityForResult(intent, 1234)
+                    }
+
+                    override fun onCancel() {
+                    }
+                },
+                "去开启",
+                "取消"
+            )
+        }
+    }
+
+
+    /**
      * 保存冥想记录
      */
     private fun saveThinkRecord() {
-        if (voice == null||startTime == 0L||SPUtils.instance().getString(Const.User.TOKEN).isNullOrEmpty())
+        if (ThinkAudioService.voice == null||startTime == 0L||SPUtils.instance().getString(Const.User.TOKEN).isNullOrEmpty())
             return
         val time = ((System.currentTimeMillis() - startTime) / 1000).toInt()
         startTime = 0L
-        HttpManager.saveViewingHistory(voice?.id ?: "", time).request(this, false, { _, _ ->
+        HttpManager.saveViewingHistory(ThinkAudioService.voice?.id ?: "", time).request(this, false, { _, _ ->
             Log.e(Const.Tag, "冥想记录成功:$time 秒")
         }){ _, _->
             Log.e(Const.Tag, "冥想记录失败:$time 秒")
@@ -344,12 +384,17 @@
             tab_bar.currentTab = 3
             onTabSelect(3)
         }else if(e.code == Const.EventCode.APP_FOREGROUND){
-            if (voice==null)
+            EasyFloat.hide(EasyFloatTag)
+            if (ThinkAudioService.voice==null)
                 bgPlayer?.resume()
         }else if(e.code == Const.EventCode.APP_BACKGROUND){
             bgPlayer?.pause()
+            if ( ThinkAudioService.playing && Settings.canDrawOverlays(this) ) {
+                showFloater()
+                EasyFloat.show(EasyFloatTag)
+            }
         }else if(e.code == Const.EventCode.CHANGE_BGM){
-            if (voice == null)
+            if (ThinkAudioService.voice == null)
                 startBgm()
             else
                 isBGMChanged = true //正在播放疗愈,无法立即切换背景音乐
@@ -359,21 +404,18 @@
             finishThink()
         }else if(e.code == Const.EventCode.PAUSE_OR_RESUME_THINK){
             player_play.callOnClick()
-        }else if(e.code == Const.EventCode.CHANGE_THINK_VOLUME){
-            val v = SPUtils.instance().getInt(Const.User.VOLUME_THINK, 50)
-            thinkBgPlayer?.setVolume(v.toFloat() / 100)
         }else if(e.code == Const.EventCode.START_GUIDE_AUDIO){
             inGuide = true
             startGuide()
             bgPlayer?.pause()
-            thinkBgPlayer?.pause()
+            EventBus.getDefault().post(EmptyEvent(Const.EventCode.SERVICE_AUDIO_PAUSE))
         }else if(e.code == Const.EventCode.FINISH_GUIDE_AUDIO){
             inGuide = false
             guidePlayer?.stopPlayMusic(false)
-            if (voice!=null&& playing){
-                thinkBgPlayer?.resume()
+            if (ThinkAudioService.voice!=null&& ThinkAudioService.playing){
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.SERVICE_AUDIO_RESUME))
             }
-            if (voice == null)
+            if (ThinkAudioService.voice == null)
                 bgPlayer?.resume()
         }else if(e.code == Const.EventCode.REFRESH_PRIVATE){ //重新答题后,刷新私人定制
             (fragments[0] as HomeFragment).getPrivacy()
@@ -385,7 +427,7 @@
     @Subscribe
     fun onIntEvent(e: IntEvent){
         if (e.code == Const.EventCode.THINK_SEEK_PROGRESS){
-            thinkBgPlayer?.seekTo(e.i)
+            EventBus.getDefault().post(IntEvent(Const.EventCode.SERVICE_AUDIO_SEEK,e.i))
             player_play.callOnClick()
         }
     }
@@ -408,6 +450,25 @@
         }
     }
 
+    private fun showFloater() {
+        if (floater == null){
+            floater = EasyFloat.with(applicationContext)
+                .setLayout(R.layout.layout_floter, OnInvokeView {
+                })
+                .setShowPattern(ShowPattern.ALL_TIME)
+                .setSidePattern(SidePattern.RESULT_LEFT)
+                .setGravity(Gravity.START or Gravity.BOTTOM, 0, 0)
+                .setDragEnable(true)
+                .setTag(EasyFloatTag)
+                .setMatchParent(widthMatch = false, heightMatch = false)
+                .registerCallback {
+                    touchEvent { view, motionEvent ->
+                        motionEvent.action
+                    }
+                }
+            floater?.show()
+        }
+    }
 
     private fun startTodayCheck(){
         thinkHandler?.sendEmptyMessage(MSG_TODAY)
@@ -460,22 +521,7 @@
     }
 
     override fun onFinishPlay() {
-        if (voice == null) //说明是手动关闭的,不需要处理下一步播放逻辑
-            return
-        if (isRecycle){ //单曲
-            if (playing)
-                thinkBgPlayer?.startPlayMusic(this, voice?.meditationMusicList?.get(index))
-        }else{//顺序
-            index++
-            if (index>=voice?.meditationMusicList?.size?:0)//列表已播完
-                EventBus.getDefault().post(EmptyEvent(Const.EventCode.FINISH_THINK))
-            else{
-                currentDuration = voice?.meditationSecondList?.get(index)?:0
-                EventBus.getDefault().post(EmptyEvent(Const.EventCode.GOT_THINK_DURATION))
-                if (playing)
-                    thinkBgPlayer?.startPlayMusic(this, voice?.meditationMusicList?.get(index))
-            }
-        }
+
     }
 
     override fun onGetDuration(duration: Int) {
@@ -487,13 +533,4 @@
             super.onBackPressed()
     }
 
-    companion object{ //冥想播放相关参数
-        var playing = false //true播放中
-        var isRecycle = false //true单曲循环播放
-        var voice: VoiceDetail? = null //冥想详情
-        var index = 0 //当前播放序号
-        var currentDuration = 0 //当前音频长度(秒)
-        var currentPosition = 0L //当前音频进度(毫秒)
-        var finishTime = 0L //自动结束的时间戳
-    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/sinata/xqmuse/ThinkAudioService.kt b/app/src/main/java/com/sinata/xqmuse/ThinkAudioService.kt
index a6d8f87..63acd19 100644
--- a/app/src/main/java/com/sinata/xqmuse/ThinkAudioService.kt
+++ b/app/src/main/java/com/sinata/xqmuse/ThinkAudioService.kt
@@ -9,17 +9,32 @@
 import android.os.Build
 import android.os.IBinder
 import android.os.PowerManager
+import android.util.Log
 import androidx.core.app.NotificationCompat
+import cn.sinata.xldutils.utils.SPUtils
+import com.sinata.xqmuse.network.entity.VoiceDetail
+import com.sinata.xqmuse.utils.AudioUtils
+import com.sinata.xqmuse.utils.Const
+import com.sinata.xqmuse.utils.event.EmptyEvent
+import com.sinata.xqmuse.utils.event.IntEvent
+import kotlinx.android.synthetic.main.activity_main.*
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
 
-class ThinkAudioService:Service() {
-    private var mediaPlayer: MediaPlayer? = null
+class ThinkAudioService:Service(), AudioUtils.OnAudioStatusUpdateListener {
+    private var thinkBgPlayer: AudioUtils? = null//冥想背景音播放器
     private var wakeLock: PowerManager.WakeLock? = null
+    private val TAG = "ThinkAudioService"
 
     override fun onCreate() {
         super.onCreate()
+        EventBus.getDefault().register(this)
+        Log.e(TAG,"ThinkAudioService初始化")
         // 初始化 MediaPlayer
-//        mediaPlayer = MediaPlayer.create(this, R.raw.audio_sample)
-        mediaPlayer!!.isLooping = true
+        if (thinkBgPlayer == null){
+            thinkBgPlayer = AudioUtils()
+            thinkBgPlayer!!.setOnAudioStatusUpdateListener(this)
+        }
 
         // 初始化 WakeLock
         val powerManager = getSystemService(POWER_SERVICE) as PowerManager
@@ -32,7 +47,7 @@
     private val focusChangeListener =
         AudioManager.OnAudioFocusChangeListener { focusChange ->
             if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
-                mediaPlayer!!.pause() // 焦点丢失时暂停播放
+                thinkBgPlayer?.pause() // 焦点丢失时暂停播放
             }
         }
 
@@ -57,33 +72,102 @@
 
         // 启动前台服务
         val notification = NotificationCompat.Builder(this, "audio_channel")
-            .setContentTitle("音频播放中")
+            .setContentTitle("正在疗愈")
             .setSmallIcon(R.mipmap.ic_launcher)
             .build()
         startForeground(1, notification)
 
-        // 请求音频焦点
-        val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
-        val result = audioManager.requestAudioFocus(
-            focusChangeListener,
-            AudioManager.STREAM_MUSIC,
-            AudioManager.AUDIOFOCUS_GAIN
-        )
-        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-            mediaPlayer?.start()
-            wakeLock?.acquire(30 * 60 * 1000L) // 申请 WakeLock
-        }
+        // 请求音频焦点 todo 请求焦点会导致首页水波纹视频暂停
+//        val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
+//        val result = audioManager.requestAudioFocus(
+//            focusChangeListener,
+//            AudioManager.STREAM_MUSIC,
+//            AudioManager.AUDIOFOCUS_GAIN
+//        )
+//        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            val volume = SPUtils.instance().getInt(Const.User.VOLUME_THINK, 50)
+            thinkBgPlayer?.setVolume(volume.toFloat() / 100)
+            thinkBgPlayer?.startPlayMusic(this, voice?.meditationMusicList?.get(index))
+            wakeLock?.acquire(60 * 60 * 1000L) // 申请 WakeLock
+//        }
         return START_STICKY
     }
 
-    override fun onDestroy() {
-        if (mediaPlayer != null) {
-            mediaPlayer!!.release()
-            mediaPlayer = null
+    @Subscribe
+    fun onAudioEvent(e: EmptyEvent){
+        when(e.code){
+            Const.EventCode.SERVICE_AUDIO_PAUSE->{
+                thinkBgPlayer?.pause()
+            }
+            Const.EventCode.SERVICE_AUDIO_RESUME->{
+                thinkBgPlayer?.resume()
+            }
+            Const.EventCode.CHANGE_THINK_VOLUME->{
+                val v = SPUtils.instance().getInt(Const.User.VOLUME_THINK, 50)
+                thinkBgPlayer?.setVolume(v.toFloat() / 100)
+            }
+            Const.EventCode.SERVICE_AUDIO_PROGRESS->{
+                currentPosition = thinkBgPlayer?.currentPosition ?: 0
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.GOT_THINK_POSITION))
+            }
         }
+    }
+
+    @Subscribe
+    fun onIntEvent(e: IntEvent){
+        if (e.code == Const.EventCode.SERVICE_AUDIO_SEEK){
+            thinkBgPlayer?.seekTo(e.i)
+        }
+    }
+
+    override fun onDestroy() {
+        EventBus.getDefault().unregister(this)
+        thinkBgPlayer?.stopPlayMusic(false)
         if (wakeLock != null && wakeLock!!.isHeld) {
             wakeLock!!.release()
         }
         super.onDestroy()
     }
+
+    override fun onUpdate(db: Double, time: Long) {
+
+    }
+
+    override fun onStop(filePath: String?) {
+    }
+
+    override fun onStartPlay() {
+    }
+
+    override fun onFinishPlay() {
+        if (voice == null) //说明是手动关闭的,不需要处理下一步播放逻辑
+            return
+        if (isRecycle){ //单曲
+            if (playing)
+                thinkBgPlayer?.startPlayMusic(this, voice?.meditationMusicList?.get(index))
+        }else{//顺序
+            index++
+            if (index>=voice?.meditationMusicList?.size?:0)//列表已播完
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.FINISH_THINK))
+            else{
+                currentDuration = voice?.meditationSecondList?.get(index)?:0
+                EventBus.getDefault().post(EmptyEvent(Const.EventCode.GOT_THINK_DURATION))
+                if (playing)
+                    thinkBgPlayer?.startPlayMusic(this, voice?.meditationMusicList?.get(index))
+            }
+        }
+    }
+
+    override fun onGetDuration(duration: Int) {
+    }
+
+    companion object{ //冥想播放相关参数
+        var playing = false //true播放中
+        var isRecycle = false //true单曲循环播放
+        var voice: VoiceDetail? = null //冥想详情
+        var index = 0 //当前播放序号
+        var currentDuration = 0 //当前音频长度(秒)
+        var currentPosition = 0L //当前音频进度(毫秒)
+        var finishTime = 0L //自动结束的时间戳
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/sinata/xqmuse/network/Apis.kt b/app/src/main/java/com/sinata/xqmuse/network/Apis.kt
index 961345f..11ac61b 100644
--- a/app/src/main/java/com/sinata/xqmuse/network/Apis.kt
+++ b/app/src/main/java/com/sinata/xqmuse/network/Apis.kt
@@ -13,7 +13,7 @@
     const val getCode = "auth/app/sendCaptchaCode"
     const val querySystemImg = "system/system/page/getPage"
 
-    const val PUSH_BASE_URL = "http://113.45.158.158/share/#/"
+    const val PUSH_BASE_URL = "https://xq.xqzhihui.com/share/#/"
     const val RANK = PUSH_BASE_URL + "pages/ranking/ranking?userId=%s"
     const val PUSH_LIST = PUSH_BASE_URL + "pages/ranking/recommend?userId=%s"
     const val SHARE_APP = PUSH_BASE_URL + "pages/poster/poster?userId=%s"
diff --git a/app/src/main/java/com/sinata/xqmuse/ui/home/HomeFragment.kt b/app/src/main/java/com/sinata/xqmuse/ui/home/HomeFragment.kt
index f67d9cc..484e879 100644
--- a/app/src/main/java/com/sinata/xqmuse/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/sinata/xqmuse/ui/home/HomeFragment.kt
@@ -11,6 +11,7 @@
 import com.google.android.exoplayer2.upstream.RawResourceDataSource
 import com.sinata.xqmuse.MainActivity
 import com.sinata.xqmuse.R
+import com.sinata.xqmuse.ThinkAudioService
 import com.sinata.xqmuse.network.HttpManager
 import com.sinata.xqmuse.network.entity.*
 import com.sinata.xqmuse.network.requestByF
@@ -101,9 +102,9 @@
             if (today!=null){
                 if (today?.isShow == 1){ //跳转播放微电影
                     startActivity<MicroVideoActivity>("url" to today?.meditationVideo?.videoUrl,"title" to today?.meditationVideo?.title)
-                }else if (MainActivity.voice?.id == today?.meditationId){
+                }else if (ThinkAudioService.voice?.id == today?.meditationId){
                     EventBus.getDefault().post(EmptyEvent(Const.EventCode.PAUSE_OR_RESUME_THINK))
-                    if (MainActivity.playing) //播放中
+                    if (ThinkAudioService.playing) //播放中
                         iv_play_today.setImageResource(R.mipmap.player_pause)
                     else
                         iv_play_today.setImageResource(R.mipmap.play)
@@ -116,9 +117,9 @@
                         }else if (data?.chargeType == 3&&data.isBuy != 1){ //单独收费且未购买
                             startActivity<BuyVoiceActivity>("id" to data.id)
                         }else{
-                            if (MainActivity.playing)
+                            if (ThinkAudioService.playing)
                                 EventBus.getDefault().post(EmptyEvent(Const.EventCode.FINISH_THINK))
-                            MainActivity.voice = data
+                            ThinkAudioService.voice = data
                             EventBus.getDefault().post(EmptyEvent(Const.EventCode.START_THINK))
                         }
                     }){_,_->
@@ -220,7 +221,7 @@
     }
 
     fun refreshTodayPlayingState(){
-        if (MainActivity.voice?.id == today?.meditationId&&MainActivity.playing)
+        if (ThinkAudioService.voice?.id == today?.meditationId&&ThinkAudioService.playing)
             iv_play_today.setImageResource(R.mipmap.player_pause)
         else
             iv_play_today.setImageResource(R.mipmap.play)
diff --git a/app/src/main/java/com/sinata/xqmuse/ui/home/VoiceDetailActivity.kt b/app/src/main/java/com/sinata/xqmuse/ui/home/VoiceDetailActivity.kt
index a08bc83..4c2cea9 100644
--- a/app/src/main/java/com/sinata/xqmuse/ui/home/VoiceDetailActivity.kt
+++ b/app/src/main/java/com/sinata/xqmuse/ui/home/VoiceDetailActivity.kt
@@ -12,6 +12,7 @@
 import com.share.utils.ShareUtils
 import com.sinata.xqmuse.MainActivity
 import com.sinata.xqmuse.R
+import com.sinata.xqmuse.ThinkAudioService
 import com.sinata.xqmuse.dialog.CommentDialog
 import com.sinata.xqmuse.dialog.ShareDialog
 import com.sinata.xqmuse.dialog.TimeSettingDialog
@@ -44,16 +45,16 @@
         }
 
         iv_play.setOnClickListener {
-            if (MainActivity.voice?.id == voiceDetail?.id){
+            if (ThinkAudioService.voice?.id == voiceDetail?.id){
                 EventBus.getDefault().post(EmptyEvent(Const.EventCode.PAUSE_OR_RESUME_THINK))
-                if (MainActivity.playing) //播放中
+                if (ThinkAudioService.playing) //播放中
                     iv_play.setImageResource(R.mipmap.player_pause)
                 else
                     iv_play.setImageResource(R.mipmap.play_detail)
             }else{
-                if (MainActivity.playing)
+                if (ThinkAudioService.playing)
                     EventBus.getDefault().post(EmptyEvent(Const.EventCode.FINISH_THINK))
-                MainActivity.voice = voiceDetail
+                ThinkAudioService.voice = voiceDetail
                 EventBus.getDefault().post(EmptyEvent(Const.EventCode.START_THINK))
                 iv_play.setImageResource(R.mipmap.player_pause)
             }
@@ -82,7 +83,7 @@
             timeSettingDialog.callback = object :StringCallback{
                 override fun onResult(rst: String) {
                     Log.e(Const.Tag,"设置倒计时$rst")
-                    MainActivity.finishTime = System.currentTimeMillis()+rst.toLong()*60*1000
+                    ThinkAudioService.finishTime = System.currentTimeMillis()+rst.toLong()*60*1000
                     EventBus.getDefault().post(EmptyEvent(Const.EventCode.THINK_TIMER))
                     startTimer()
                 }
@@ -91,10 +92,10 @@
         }
 
         iv_recycle.setOnClickListener {
-            MainActivity.isRecycle = !MainActivity.isRecycle
-            iv_recycle.setImageResource(if (MainActivity.isRecycle) R.mipmap.danquxunhuan else R.mipmap.ic_recycle)
-            whiteToast(if (MainActivity.isRecycle) "当前播放模式已设置为单曲循环" else "当前播放模式已设置为顺序播放")
-            SPUtils.instance().put(Const.User.IS_RECYCLE,MainActivity.isRecycle).apply()
+            ThinkAudioService.isRecycle = !ThinkAudioService.isRecycle
+            iv_recycle.setImageResource(if (ThinkAudioService.isRecycle) R.mipmap.danquxunhuan else R.mipmap.ic_recycle)
+            whiteToast(if (ThinkAudioService.isRecycle) "当前播放模式已设置为单曲循环" else "当前播放模式已设置为顺序播放")
+            SPUtils.instance().put(Const.User.IS_RECYCLE,ThinkAudioService.isRecycle).apply()
         }
         tv_comment.setOnClickListener {
             val commentDialog = CommentDialog()
@@ -111,14 +112,14 @@
             }
 
             override fun onStartTrackingTouch(seekBar: SeekBar?) {
-                if (MainActivity.playing){ //播放中,让其暂停,并让按钮无法点击
+                if (ThinkAudioService.playing){ //播放中,让其暂停,并让按钮无法点击
                     iv_play.isEnabled = false
                     EventBus.getDefault().post(EmptyEvent(Const.EventCode.PAUSE_OR_RESUME_THINK))
                 }
             }
 
             override fun onStopTrackingTouch(seekBar: SeekBar?) {
-                if (!MainActivity.playing){//暂停时,让其播放并让按钮恢复可点击
+                if (!ThinkAudioService.playing){//暂停时,让其播放并让按钮恢复可点击
                     EventBus.getDefault().post(IntEvent(Const.EventCode.THINK_SEEK_PROGRESS,seekBar?.progress?:0))
                     iv_play.isEnabled = true
                 }
@@ -128,25 +129,25 @@
 
     override fun initView() {
         titleBar.gone()
-        MainActivity.isRecycle = SPUtils.instance().getBoolean(Const.User.IS_RECYCLE,false)
-        iv_recycle.setImageResource(if (MainActivity.isRecycle) R.mipmap.danquxunhuan else R.mipmap.ic_recycle)
+        ThinkAudioService.isRecycle = SPUtils.instance().getBoolean(Const.User.IS_RECYCLE,false)
+        iv_recycle.setImageResource(if (ThinkAudioService.isRecycle) R.mipmap.danquxunhuan else R.mipmap.ic_recycle)
         voiceDetail?.apply {
             iv_collect.setImageResource(if (favorite == 1) R.mipmap.collected else R.mipmap.uncollect)
             iv_bg.setImageURI(backgroundUrl)
             tv_name.text = meditationTitle
             tv_subtitle.text = detailDescription
             tv_comment.text = questionCount
-            val seconds = (if (MainActivity.voice?.id == id) meditationSecondList?.getOrNull(MainActivity.index) else meditationSecondList?.firstOrNull() )?: 0
+            val seconds = (if (ThinkAudioService.voice?.id == id) meditationSecondList?.getOrNull(ThinkAudioService.index) else meditationSecondList?.firstOrNull() )?: 0
             tv_total.text = "%02d:%02d".format(seconds/60,seconds%60)
             sb_voice.max = seconds
-            if (MainActivity.voice?.id == id){  //主页播放的正是本音频
-                if (MainActivity.finishTime!=0L)//有倒计时存在
+            if (ThinkAudioService.voice?.id == id){  //主页播放的正是本音频
+                if (ThinkAudioService.finishTime!=0L)//有倒计时存在
                     startTimer()
-                if (MainActivity.playing) //如果在播放中,按钮变为暂停
+                if (ThinkAudioService.playing) //如果在播放中,按钮变为暂停
                     iv_play.setImageResource(R.mipmap.player_pause)
                 //恢复进度
-                tv_progress.text = "%02d:%02d".format(MainActivity.currentPosition/1000/60,MainActivity.currentPosition/1000%60)
-                sb_voice.progress = (MainActivity.currentPosition/1000).toInt()
+                tv_progress.text = "%02d:%02d".format(ThinkAudioService.currentPosition/1000/60,ThinkAudioService.currentPosition/1000%60)
+                sb_voice.progress = (ThinkAudioService.currentPosition/1000).toInt()
             }
         }
         EventBus.getDefault().register(this)
@@ -157,7 +158,7 @@
         if (countDownTimer!=null)
             countDownTimer!!.cancel()
         tv_timer.visible()
-        val offset = MainActivity.finishTime - System.currentTimeMillis()
+        val offset = ThinkAudioService.finishTime - System.currentTimeMillis()
         countDownTimer = object :CountDownTimer(offset,1000){
             override fun onTick(millisUntilFinished: Long) {
                 tv_timer.text = "%02d:%02d".format(millisUntilFinished/1000/60,millisUntilFinished/1000%60)
@@ -185,12 +186,12 @@
             tv_progress.text = "00:00"
             tv_timer.gone()
             countDownTimer?.cancel()
-        }else if (e.code == Const.EventCode.GOT_THINK_DURATION&&MainActivity.voice?.id == voiceDetail?.id){
-            tv_total.text = "%02d:%02d".format(MainActivity.currentDuration/60,MainActivity.currentDuration%60)
-            sb_voice.max = MainActivity.currentDuration
-        }else if (e.code == Const.EventCode.GOT_THINK_POSITION&&MainActivity.voice?.id == voiceDetail?.id){
-            tv_progress.text = "%02d:%02d".format(MainActivity.currentPosition/1000/60,MainActivity.currentPosition/1000%60)
-            sb_voice.progress = (MainActivity.currentPosition/1000).toInt()
+        }else if (e.code == Const.EventCode.GOT_THINK_DURATION&&ThinkAudioService.voice?.id == voiceDetail?.id){
+            tv_total.text = "%02d:%02d".format(ThinkAudioService.currentDuration/60,ThinkAudioService.currentDuration%60)
+            sb_voice.max = ThinkAudioService.currentDuration
+        }else if (e.code == Const.EventCode.GOT_THINK_POSITION&&ThinkAudioService.voice?.id == voiceDetail?.id){
+            tv_progress.text = "%02d:%02d".format(ThinkAudioService.currentPosition/1000/60,ThinkAudioService.currentPosition/1000%60)
+            sb_voice.progress = (ThinkAudioService.currentPosition/1000).toInt()
         }
     }
 
diff --git a/app/src/main/java/com/sinata/xqmuse/utils/Const.kt b/app/src/main/java/com/sinata/xqmuse/utils/Const.kt
index c6f3bcd..8e2413c 100644
--- a/app/src/main/java/com/sinata/xqmuse/utils/Const.kt
+++ b/app/src/main/java/com/sinata/xqmuse/utils/Const.kt
@@ -94,6 +94,13 @@
         const val FINISH_GUIDE_AUDIO = 0x1F //停止引导音
         const val REFRESH_PRIVATE = 0x20 //刷新私人定制
 
+        //音频服务事件
+        const val SERVICE_AUDIO_PAUSE = 0x21 //暂停音乐
+        const val SERVICE_AUDIO_RESUME = 0x22 //继续音乐
+        const val SERVICE_AUDIO_VOLUME = 0x23 //音量变化
+        const val SERVICE_AUDIO_SEEK = 0x24 //进度拖动
+        const val SERVICE_AUDIO_PROGRESS = 0x25 //获取新的播放进度
+
 
     }
 }
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_floter.xml b/app/src/main/res/layout/layout_floter.xml
new file mode 100644
index 0000000..5d34a0b
--- /dev/null
+++ b/app/src/main/res/layout/layout_floter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="2dp"
+        android:layout_height="2dp"
+        android:background="@color/transparent">
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/easyfloat/.gitignore b/easyfloat/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/easyfloat/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/easyfloat/build.gradle b/easyfloat/build.gradle
new file mode 100644
index 0000000..19f6cef
--- /dev/null
+++ b/easyfloat/build.gradle
@@ -0,0 +1,37 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-android'
+group = 'com.github.princekin-f'
+
+android {
+    compileSdkVersion 28
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    api 'org.greenrobot:eventbus:3.1.1'
+}
+repositories {
+    mavenCentral()
+}
diff --git a/easyfloat/proguard-rules.pro b/easyfloat/proguard-rules.pro
new file mode 100644
index 0000000..201c2e7
--- /dev/null
+++ b/easyfloat/proguard-rules.pro
@@ -0,0 +1,37 @@
+# Add project specific ProGuard rules here.
+# You can control the filterSet of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+# 保持配置类 config 不被混淆
+-keep class com.lzf.easyfloat.data.FloatConfig {*;}
+
+# 保持自定义控件、ContentProvider 不被混淆
+-keep public class * extends android.view.View
+-keep public class * extends android.content.ContentProvider
+
+# 保持枚举 enum 类不被混淆
+-keepclassmembers enum * {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+# 保持反射不被混淆
+-keepattributes EnclosingMethod
\ No newline at end of file
diff --git a/easyfloat/src/androidTest/java/com/lzf/easyfloat/ExampleInstrumentedTest.java b/easyfloat/src/androidTest/java/com/lzf/easyfloat/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..eb224f1
--- /dev/null
+++ b/easyfloat/src/androidTest/java/com/lzf/easyfloat/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.lzf.easyfloat;
+
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("com.lzf.easyfloat.test", appContext.getPackageName());
+    }
+}
diff --git a/easyfloat/src/main/AndroidManifest.xml b/easyfloat/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..abd6189
--- /dev/null
+++ b/easyfloat/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.lzf.easyfloat">
+
+    <application>
+        <provider
+                android:name="com.lzf.easyfloat.EasyFloatInitializer"
+                android:authorities="${applicationId}.EasyFloatInitializer"
+                android:exported="false"
+                android:multiprocess="true" />
+    </application>
+
+</manifest>
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/BaseEventWindow.kt b/easyfloat/src/main/java/com/lzf/easyfloat/BaseEventWindow.kt
new file mode 100644
index 0000000..40ad766
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/BaseEventWindow.kt
@@ -0,0 +1,4 @@
+package com.lzf.easyfloat
+
+open class BaseEventWindow {
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloat.kt b/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloat.kt
new file mode 100644
index 0000000..490f7fc
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloat.kt
@@ -0,0 +1,379 @@
+package com.lzf.easyfloat
+
+import android.app.Activity
+import android.content.Context
+import android.view.View
+import com.lzf.easyfloat.core.FloatingWindowManager
+import com.lzf.easyfloat.data.FloatConfig
+import com.lzf.easyfloat.enums.ShowPattern
+import com.lzf.easyfloat.enums.SidePattern
+import com.lzf.easyfloat.interfaces.*
+import com.lzf.easyfloat.interfaces.OnPermissionResult
+import com.lzf.easyfloat.permission.PermissionUtils
+import com.lzf.easyfloat.utils.LifecycleUtils
+import com.lzf.easyfloat.interfaces.FloatCallbacks
+import com.lzf.easyfloat.utils.DisplayUtils
+import com.lzf.easyfloat.utils.Logger
+import org.greenrobot.eventbus.EventBus
+import java.lang.Exception
+
+/**
+ * @author: liuzhenfeng
+ * @github:https://github.com/princekin-f
+ * @function: 悬浮窗使用工具类
+ * @date: 2019-06-27  15:22
+ */
+class EasyFloat {
+
+    companion object {
+
+        /**
+         * 通过上下文,创建浮窗的构建者信息,使浮窗拥有一些默认属性
+         * @param activity 上下文信息,优先使用Activity上下文,因为系统浮窗权限的自动申请,需要使用Activity信息
+         * @return 浮窗属性构建者
+         */
+        @JvmStatic
+        fun with(activity: Context): Builder = if (activity is Activity) Builder(activity)
+        else Builder(LifecycleUtils.getTopActivity() ?: activity)
+
+        /**
+         * 关闭当前浮窗
+         * @param tag 浮窗标签
+         * @param force 立即关闭,有退出动画也不执行
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun dismiss(tag: String? = null, force: Boolean = false) =
+            FloatingWindowManager.dismiss(tag, force)
+
+        /**
+         * 隐藏当前浮窗
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun hide(tag: String? = null) = FloatingWindowManager.visible(false, tag, false)
+
+        /**
+         * 设置当前浮窗可见
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun show(tag: String? = null) = FloatingWindowManager.visible(true, tag, true)
+
+        /**
+         * 设置当前浮窗是否可拖拽,先获取浮窗的config,后修改相应属性
+         * @param dragEnable 是否可拖拽
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun dragEnable(dragEnable: Boolean, tag: String? = null) =
+            getConfig(tag)?.let { it.dragEnable = dragEnable }
+
+        /**
+         * 获取当前浮窗是否显示,通过浮窗的config,获取显示状态
+         * @param tag 浮窗标签
+         * @return 当前浮窗是否显示
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun isShow(tag: String? = null) = getConfig(tag)?.isShow ?: false
+
+        /**
+         * 获取当前浮窗中,我们传入的View
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun getFloatView(tag: String? = null): View? = getConfig(tag)?.layoutView
+
+        /**
+         * 更新浮窗坐标,未指定坐标执行吸附动画
+         * @param tag 浮窗标签
+         * @param x 更新后的X轴坐标
+         * @param y 更新后的Y轴坐标
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun updateFloat(tag: String? = null, x: Int = -1, y: Int = -1) =
+            FloatingWindowManager.getHelper(tag)?.updateFloat(x, y)
+
+        // 以下几个方法为:系统浮窗过滤页面的添加、移除、清空
+        /**
+         * 为当前浮窗过滤,设置需要过滤的Activity
+         * @param activity 需要过滤的Activity
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun filterActivity(activity: Activity, tag: String? = null) =
+            getFilterSet(tag)?.add(activity.componentName.className)
+
+        /**
+         * 为当前浮窗,设置需要过滤的Activity类名(一个或者多个)
+         * @param tag 浮窗标签
+         * @param clazz 需要过滤的Activity类名,一个或者多个
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun filterActivities(tag: String? = null, vararg clazz: Class<*>) =
+            getFilterSet(tag)?.addAll(clazz.map { it.name })
+
+        /**
+         * 为当前浮窗,移除需要过滤的Activity
+         * @param activity 需要移除过滤的Activity
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun removeFilter(activity: Activity, tag: String? = null) =
+            getFilterSet(tag)?.remove(activity.componentName.className)
+
+        /**
+         * 为当前浮窗,移除需要过滤的Activity类名(一个或者多个)
+         * @param tag 浮窗标签
+         * @param clazz 需要移除过滤的Activity类名,一个或者多个
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun removeFilters(tag: String? = null, vararg clazz: Class<*>) =
+            getFilterSet(tag)?.removeAll(clazz.map { it.name })
+
+        /**
+         * 清除当前浮窗的所有过滤信息
+         * @param tag 浮窗标签
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun clearFilters(tag: String? = null) = getFilterSet(tag)?.clear()
+
+        /**
+         * 获取当前浮窗的config
+         * @param tag 浮窗标签
+         */
+        private fun getConfig(tag: String?) = FloatingWindowManager.getHelper(tag)?.config
+
+        /**
+         * 获取当前浮窗的过滤集合
+         * @param tag 浮窗标签
+         */
+        private fun getFilterSet(tag: String?) = getConfig(tag)?.filterSet
+    }
+
+
+    /**
+     * 浮窗的属性构建类,支持链式调用
+     */
+    class Builder(private val activity: Context) : OnPermissionResult {
+
+        // 创建浮窗数据类,方便管理配置
+        private val config = FloatConfig()
+
+        /**
+         * 设置浮窗的吸附模式
+         * @param sidePattern 浮窗吸附模式
+         */
+        fun setSidePattern(sidePattern: SidePattern) = apply { config.sidePattern = sidePattern }
+
+        /**
+         * 设置浮窗的显示模式
+         * @param showPattern 浮窗显示模式
+         */
+        fun setShowPattern(showPattern: ShowPattern) = apply { config.showPattern = showPattern }
+
+        /**
+         * 设置浮窗的布局文件,以及布局的操作接口
+         * @param layoutId 布局文件的资源Id
+         * @param invokeView 布局文件的操作接口
+         */
+        @JvmOverloads
+        fun setLayout(layoutId: Int, invokeView: OnInvokeView? = null) = apply {
+            config.layoutId = layoutId
+            config.invokeView = invokeView
+        }
+
+        /**
+         * 设置浮窗的对齐方式,以及偏移量
+         * @param gravity 对齐方式
+         * @param offsetX 目标坐标的水平偏移量
+         * @param offsetY 目标坐标的竖直偏移量
+         */
+        @JvmOverloads
+        fun setGravity(gravity: Int, offsetX: Int = 0, offsetY: Int = 0) = apply {
+            config.gravity = gravity
+            config.offsetPair = Pair(offsetX, offsetY)
+        }
+
+        /**
+         * 设置浮窗的起始坐标,优先级高于setGravity
+         * @param x 起始水平坐标
+         * @param y 起始竖直坐标
+         */
+        fun setLocation(x: Int, y: Int) = apply { config.locationPair = Pair(x, y) }
+
+        /**
+         * 设置浮窗的拖拽边距值
+         * @param left 浮窗左侧边距
+         * @param top 浮窗顶部边距
+         * @param right 浮窗右侧边距
+         * @param bottom 浮窗底部边距
+         */
+        @JvmOverloads
+        fun setBorder(
+            left: Int = 0,
+            top: Int = -DisplayUtils.getStatusBarHeight(activity),
+            right: Int = DisplayUtils.getScreenWidth(activity),
+            bottom: Int = DisplayUtils.getScreenHeight(activity)
+        ) = apply {
+            config.leftBorder = left
+            config.topBorder = top
+            config.rightBorder = right
+            config.bottomBorder = bottom
+        }
+
+        /**
+         * 设置浮窗的标签:只有一个浮窗时,可以不设置;
+         * 有多个浮窗必须设置不容的浮窗,不然没法管理,所以禁止创建相同标签的浮窗
+         * @param floatTag 浮窗标签
+         */
+        fun setTag(floatTag: String?) = apply { config.floatTag = floatTag }
+
+        /**
+         * 设置浮窗是否可拖拽
+         * @param dragEnable 是否可拖拽
+         */
+        fun setDragEnable(dragEnable: Boolean) = apply { config.dragEnable = dragEnable }
+
+        /**
+         * 设置浮窗是否状态栏沉浸
+         * @param immersionStatusBar 是否状态栏沉浸
+         */
+        fun setImmersionStatusBar(immersionStatusBar: Boolean) =
+            apply { config.immersionStatusBar = immersionStatusBar }
+
+        /**
+         * 浮窗是否包含EditText,浮窗默认不获取焦点,无法弹起软键盘,所以需要适配
+         * @param hasEditText 是否包含EditText
+         */
+        fun hasEditText(hasEditText: Boolean) = apply { config.hasEditText = hasEditText }
+
+        /**
+         * 通过传统接口,进行浮窗的各种状态回调
+         * @param callbacks 浮窗的各种事件回调
+         */
+        fun registerCallbacks(callbacks: OnFloatCallbacks) = apply { config.callbacks = callbacks }
+
+        /**
+         * 针对kotlin 用户,传入带FloatCallbacks.Builder 返回值的 lambda,可按需回调
+         * 为了避免方法重载时 出现编译错误的情况,更改了方法名
+         * @param builder 事件回调的构建者
+         */
+        fun registerCallback(builder: FloatCallbacks.Builder.() -> Unit) =
+            apply { config.floatCallbacks = FloatCallbacks().apply { registerListener(builder) } }
+
+        /**
+         * 设置浮窗的出入动画
+         * @param floatAnimator 浮窗的出入动画,为空时不执行动画
+         */
+        fun setAnimator(floatAnimator: OnFloatAnimator?) =
+            apply { config.floatAnimator = floatAnimator }
+
+        /**
+         * 设置屏幕的有效显示高度(不包含虚拟导航栏的高度)
+         * @param displayHeight 屏幕的有效高度
+         */
+        fun setDisplayHeight(displayHeight: OnDisplayHeight) =
+            apply { config.displayHeight = displayHeight }
+
+        /**
+         * 设置浮窗宽高是否充满屏幕
+         * @param widthMatch 宽度是否充满屏幕
+         * @param heightMatch 高度是否充满屏幕
+         */
+        fun setMatchParent(widthMatch: Boolean = false, heightMatch: Boolean = false) = apply {
+            config.widthMatch = widthMatch
+            config.heightMatch = heightMatch
+        }
+
+        /**
+         * 设置需要过滤的Activity类名,仅对系统浮窗有效
+         * @param clazz 需要过滤的Activity类名
+         */
+        fun setFilter(vararg clazz: Class<*>) = apply {
+            clazz.forEach {
+                config.filterSet.add(it.name)
+                if (activity is Activity) {
+                    // 过滤掉当前Activity
+                    if (it.name == activity.componentName.className) config.filterSelf = true
+                }
+            }
+        }
+
+        /**
+         * 创建浮窗,包括Activity浮窗和系统浮窗,如若系统浮窗无权限,先进行权限申请
+         */
+        fun show() = when {
+            // 未设置浮窗布局文件,不予创建
+            config.layoutId == null -> callbackCreateFailed(WARN_NO_LAYOUT)
+            // 仅当页显示,则直接创建activity浮窗
+            config.showPattern == ShowPattern.CURRENT_ACTIVITY -> createFloat()
+            // 系统浮窗需要先进行权限审核,有权限则创建app浮窗
+            PermissionUtils.checkPermission(activity) -> createFloat()
+            // 申请浮窗权限
+            else -> {
+                WindowDIalog(activity,object : WindowDIalog.OnDialogListener{
+                    override fun onClickImgCancle() {
+                    }
+
+                    override fun onClickCancle() {
+                    }
+
+                    override fun onClickSure(position: Int?) {
+                        requestPermission()
+                    }
+
+                }).show()
+
+            }
+        }
+
+        /**
+         * 通过浮窗管理类,统一创建浮窗
+         */
+        private fun createFloat() = FloatingWindowManager.create(activity, config)
+
+        /**
+         * 通过Fragment去申请系统悬浮窗权限
+         */
+        private fun requestPermission() =
+            if (activity is Activity) PermissionUtils.requestPermission(activity, this)
+            else callbackCreateFailed(WARN_CONTEXT_REQUEST)
+
+        /**
+         * 申请浮窗权限的结果回调
+         * @param isOpen 悬浮窗权限是否打开
+         */
+        override fun permissionResult(isOpen: Boolean) =
+            if (isOpen) {
+//                EventBus.getDefault().post(WindowEvent(127))
+//                createFloat()
+            } else callbackCreateFailed(WARN_PERMISSION)
+
+        /**
+         * 回调创建失败
+         * @param reason 失败原因
+         */
+        private fun callbackCreateFailed(reason: String) {
+            config.callbacks?.createdResult(false, reason, null)
+            config.floatCallbacks?.builder?.createdResult?.invoke(false, reason, null)
+            Logger.w(reason)
+            if (reason == WARN_NO_LAYOUT || reason == WARN_UNINITIALIZED || reason == WARN_CONTEXT_ACTIVITY) {
+                // 针对无布局、未按需初始化、Activity浮窗上下文错误,直接抛异常
+                throw Exception(reason)
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatInitializer.kt b/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatInitializer.kt
new file mode 100644
index 0000000..2df6f95
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatInitializer.kt
@@ -0,0 +1,43 @@
+package com.lzf.easyfloat
+
+import android.app.Application
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.net.Uri
+import com.lzf.easyfloat.utils.LifecycleUtils
+
+/**
+ * @author: liuzhenfeng
+ * @github:https://github.com/princekin-f
+ * @function:
+ * @date: 2020/10/23  13:41
+ */
+class EasyFloatInitializer : ContentProvider() {
+
+    override fun onCreate(): Boolean {
+        LifecycleUtils.setLifecycleCallbacks(context!!.applicationContext as Application)
+        return true
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? = null
+
+    override fun getType(uri: Uri): String? = null
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int = 0
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatMessage.kt b/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatMessage.kt
new file mode 100644
index 0000000..c1c173a
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatMessage.kt
@@ -0,0 +1,14 @@
+package com.lzf.easyfloat
+
+/**
+ * @author: liuzhenfeng
+ * @github:https://github.com/princekin-f
+ * @function:
+ * @date: 2020/4/10  14:25
+ */
+const val WARN_PERMISSION = "No permission exception. You need to turn on overlay permissions."
+const val WARN_NO_LAYOUT = "No layout exception. You need to set up the layout file."
+const val WARN_UNINITIALIZED = "Uninitialized exception. You need to initialize in the application."
+const val WARN_REPEATED_TAG = "Tag exception. You need to set different EasyFloat tag."
+const val WARN_CONTEXT_ACTIVITY = "Context exception. Activity float need to pass in a activity context."
+const val WARN_CONTEXT_REQUEST = "Context exception. Request Permission need to pass in a activity context."
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/WindowDIalog.kt b/easyfloat/src/main/java/com/lzf/easyfloat/WindowDIalog.kt
new file mode 100644
index 0000000..922a8a6
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/WindowDIalog.kt
@@ -0,0 +1,76 @@
+package com.lzf.easyfloat
+
+import android.app.Dialog
+import android.content.Context
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.ImageView
+import android.widget.TextView
+
+open class WindowDIalog(var mcontext: Context,var mListener : OnDialogListener) : Dialog(mcontext!!, R.style.WindowBottomDialog) {
+
+    private var btn_vertical_sure : TextView?= null
+    private var img_close : ImageView?= null
+
+    init {
+
+        setContentView(R.layout.window_dialog_common)
+
+        setCancelable(false)
+
+        initView()
+
+        setListener()
+    }
+
+     fun setListener() {
+         img_close?.setOnClickListener {
+             this@WindowDIalog.dismiss()
+             if (mListener != null) {
+                 mListener?.onClickImgCancle()
+             }
+         }
+
+         btn_vertical_sure?.setOnClickListener {
+             this@WindowDIalog.dismiss()
+             if (mListener != null) {
+                 mListener?.onClickSure(null)
+             }
+         }
+
+     }
+
+    private fun initView() {
+        btn_vertical_sure = findViewById(R.id.btn_vertical_sure)
+        img_close = findViewById(R.id.img_close)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        window?.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+        window?.decorView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+    }
+
+    override fun show() {
+        this.window?.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
+        this.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
+        var window = this.window
+        window?.decorView?.setPadding(0, 0, 0, 0)
+        window?.attributes?.height = ViewGroup.LayoutParams.WRAP_CONTENT
+        window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
+        window?.setBackgroundDrawable(mcontext?.resources?.getDrawable(R.color.transparent))
+        window?.setGravity(Gravity.CENTER)
+        super.show()
+    }
+
+
+    interface OnDialogListener{
+        fun onClickImgCancle()
+        fun onClickCancle()
+        fun onClickSure(position : Int?)
+    }
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/WindowEvent.kt b/easyfloat/src/main/java/com/lzf/easyfloat/WindowEvent.kt
new file mode 100644
index 0000000..1c8ae36
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/WindowEvent.kt
@@ -0,0 +1,3 @@
+package com.lzf.easyfloat
+
+data class WindowEvent(var type : Int) : BaseEventWindow()
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/anim/AnimatorManager.kt b/easyfloat/src/main/java/com/lzf/easyfloat/anim/AnimatorManager.kt
new file mode 100644
index 0000000..cde32e2
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/anim/AnimatorManager.kt
@@ -0,0 +1,25 @@
+package com.lzf.easyfloat.anim
+
+import android.animation.Animator
+import android.view.View
+import android.view.WindowManager
+import com.lzf.easyfloat.data.FloatConfig
+
+/**
+ * @author: liuzhenfeng
+ * @function: App浮窗的出入动画管理类,只需传入具体的动画实现类(策略模式)
+ * @date: 2019-07-22  16:44
+ */
+internal class AnimatorManager(
+    private val view: View,
+    private val params: WindowManager.LayoutParams,
+    private val windowManager: WindowManager,
+    private val config: FloatConfig
+) {
+
+    fun enterAnim(): Animator? =
+        config.floatAnimator?.enterAnim(view, params, windowManager, config.sidePattern)
+
+    fun exitAnim(): Animator? =
+        config.floatAnimator?.exitAnim(view, params, windowManager, config.sidePattern)
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/anim/DefaultAnimator.kt b/easyfloat/src/main/java/com/lzf/easyfloat/anim/DefaultAnimator.kt
new file mode 100644
index 0000000..bef36ce
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/anim/DefaultAnimator.kt
@@ -0,0 +1,146 @@
+package com.lzf.easyfloat.anim
+
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.graphics.Rect
+import android.view.View
+import android.view.WindowManager
+import com.lzf.easyfloat.enums.SidePattern
+import com.lzf.easyfloat.interfaces.OnFloatAnimator
+import com.lzf.easyfloat.utils.DisplayUtils
+import kotlin.math.min
+
+/**
+ * @author: liuzhenfeng
+ * @function: 系统浮窗的默认效果,选择靠近左右侧的一边进行出入
+ * @date: 2019-07-22  17:22
+ */
+open class DefaultAnimator : OnFloatAnimator {
+
+    override fun enterAnim(
+        view: View,
+        params: WindowManager.LayoutParams,
+        windowManager: WindowManager,
+        sidePattern: SidePattern
+    ): Animator? = getAnimator(view, params, windowManager, sidePattern, false)
+
+    override fun exitAnim(
+        view: View,
+        params: WindowManager.LayoutParams,
+        windowManager: WindowManager,
+        sidePattern: SidePattern
+    ): Animator? = getAnimator(view, params, windowManager, sidePattern, true)
+
+    private fun getAnimator(
+        view: View,
+        params: WindowManager.LayoutParams,
+        windowManager: WindowManager,
+        sidePattern: SidePattern,
+        isExit: Boolean
+    ): Animator {
+        val triple = initValue(view, params, windowManager, sidePattern)
+        // 退出动画的起始值、终点值,与入场动画相反
+        val start = if (isExit) triple.second else triple.first
+        val end = if (isExit) triple.first else triple.second
+        return ValueAnimator.ofInt(start, end).apply {
+            addUpdateListener {
+                try {
+                    val value = it.animatedValue as Int
+                    if (triple.third) params.x = value else params.y = value
+                    // 动画执行过程中页面关闭,出现异常
+                    windowManager.updateViewLayout(view, params)
+                } catch (e: Exception) {
+                    cancel()
+                }
+            }
+        }
+    }
+
+    /**
+     * 计算边距,起始坐标等
+     */
+    private fun initValue(
+        view: View,
+        params: WindowManager.LayoutParams,
+        windowManager: WindowManager,
+        sidePattern: SidePattern
+    ): Triple<Int, Int, Boolean> {
+        val parentRect = Rect()
+        windowManager.defaultDisplay.getRectSize(parentRect)
+        // 浮窗各边到窗口边框的距离
+        val leftDistance = params.x
+        val rightDistance = parentRect.right - (leftDistance + view.right)
+        val topDistance = params.y
+        val bottomDistance = parentRect.bottom - (topDistance + view.bottom)
+        // 水平、垂直方向的距离最小值
+        val minX = min(leftDistance, rightDistance)
+        val minY = min(topDistance, bottomDistance)
+
+        val isHorizontal: Boolean
+        val endValue: Int
+        val startValue: Int = when (sidePattern) {
+            SidePattern.LEFT, SidePattern.RESULT_LEFT -> {
+                // 从左侧到目标位置,右移
+                isHorizontal = true
+                endValue = params.x
+                -view.right
+            }
+            SidePattern.RIGHT, SidePattern.RESULT_RIGHT -> {
+                // 从右侧到目标位置,左移
+                isHorizontal = true
+                endValue = params.x
+                parentRect.right
+            }
+            SidePattern.TOP, SidePattern.RESULT_TOP -> {
+                // 从顶部到目标位置,下移
+                isHorizontal = false
+                endValue = params.y
+                -view.bottom
+            }
+            SidePattern.BOTTOM, SidePattern.RESULT_BOTTOM -> {
+                // 从底部到目标位置,上移
+                isHorizontal = false
+                endValue = params.y
+                parentRect.bottom + getCompensationHeight(view, params)
+            }
+
+            SidePattern.DEFAULT, SidePattern.AUTO_HORIZONTAL, SidePattern.RESULT_HORIZONTAL -> {
+                // 水平位移,哪边距离屏幕近,从哪侧移动
+                isHorizontal = true
+                endValue = params.x
+                if (leftDistance < rightDistance) -view.right else parentRect.right
+            }
+            SidePattern.AUTO_VERTICAL, SidePattern.RESULT_VERTICAL -> {
+                // 垂直位移,哪边距离屏幕近,从哪侧移动
+                isHorizontal = false
+                endValue = params.y
+                if (topDistance < bottomDistance) -view.bottom
+                else parentRect.bottom + getCompensationHeight(view, params)
+            }
+
+            else -> if (minX <= minY) {
+                isHorizontal = true
+                endValue = params.x
+                if (leftDistance < rightDistance) -view.right else parentRect.right
+            } else {
+                isHorizontal = false
+                endValue = params.y
+                if (topDistance < bottomDistance) -view.bottom
+                else parentRect.bottom + getCompensationHeight(view, params)
+            }
+        }
+        return Triple(startValue, endValue, isHorizontal)
+    }
+
+    /**
+     * 单页面浮窗(popupWindow),坐标从顶部计算,需要加上状态栏的高度
+     */
+    private fun getCompensationHeight(view: View, params: WindowManager.LayoutParams): Int {
+        val location = IntArray(2)
+        // 获取在整个屏幕内的绝对坐标
+        view.getLocationOnScreen(location)
+        // 绝对高度和相对高度相等,说明是单页面浮窗(popupWindow),计算底部动画时需要加上状态栏高度
+        return if (location[1] == params.y) DisplayUtils.statusBarHeight(view) else 0
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowHelper.kt b/easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowHelper.kt
new file mode 100644
index 0000000..a8ce6d7
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowHelper.kt
@@ -0,0 +1,337 @@
+package com.lzf.easyfloat.core
+
+import android.animation.Animator
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Service
+import android.content.Context
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.os.Build
+import android.os.IBinder
+import android.view.*
+import android.view.WindowManager.LayoutParams.*
+import android.widget.EditText
+import com.lzf.easyfloat.anim.AnimatorManager
+import com.lzf.easyfloat.data.FloatConfig
+import com.lzf.easyfloat.enums.ShowPattern
+import com.lzf.easyfloat.interfaces.OnFloatTouchListener
+import com.lzf.easyfloat.utils.DisplayUtils
+import com.lzf.easyfloat.utils.InputMethodUtils
+import com.lzf.easyfloat.utils.LifecycleUtils
+import com.lzf.easyfloat.utils.Logger
+import com.lzf.easyfloat.widget.ParentFrameLayout
+
+/**
+ * @author: Liuzhenfeng
+ * @date: 12/1/20  23:40
+ * @Description:
+ */
+internal class FloatingWindowHelper(val context: Context, var config: FloatConfig) {
+
+    lateinit var windowManager: WindowManager
+    lateinit var params: WindowManager.LayoutParams
+    var frameLayout: ParentFrameLayout? = null
+    private lateinit var touchUtils: TouchUtils
+    private var enterAnimator: Animator? = null
+
+    fun createWindow() = try {
+        touchUtils = TouchUtils(context, config)
+        initParams()
+        addView()
+        config.isShow = true
+    } catch (e: Exception) {
+        config.callbacks?.createdResult(false, "$e", null)
+        config.floatCallbacks?.builder?.createdResult?.invoke(false, "$e", null)
+    }
+
+    private fun initParams() {
+        windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
+        params = WindowManager.LayoutParams().apply {
+            if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
+                // 设置窗口类型为应用子窗口,和PopupWindow同类型
+                type = TYPE_APPLICATION_PANEL
+                // 子窗口必须和创建它的Activity的windowToken绑定
+                token = getToken()
+            } else {
+                // 系统全局窗口,可覆盖在任何应用之上,以及单独显示在桌面上
+                // 安卓6.0 以后,全局的Window类别,必须使用TYPE_APPLICATION_OVERLAY
+                type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) TYPE_APPLICATION_OVERLAY
+                else TYPE_PHONE
+            }
+            format = PixelFormat.RGBA_8888
+            gravity = Gravity.START or Gravity.TOP
+            // 设置浮窗以外的触摸事件可以传递给后面的窗口、不自动获取焦点
+            flags = if (config.immersionStatusBar)
+            // 没有边界限制,允许窗口扩展到屏幕外
+                FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE or FLAG_LAYOUT_NO_LIMITS
+            else FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
+            width = if (config.widthMatch) MATCH_PARENT else WRAP_CONTENT
+            height = if (config.heightMatch) MATCH_PARENT else WRAP_CONTENT
+
+            if (config.immersionStatusBar && config.heightMatch) {
+                height = DisplayUtils.getScreenHeight(context)
+            }
+
+            // 如若设置了固定坐标,直接定位
+            if (config.locationPair != Pair(0, 0)) {
+                x = config.locationPair.first
+                y = config.locationPair.second
+            }
+        }
+    }
+
+    private fun getToken(): IBinder? {
+        val activity = if (context is Activity) context else LifecycleUtils.getTopActivity()
+        return activity?.window?.decorView?.windowToken
+    }
+
+    /**
+     * 将自定义的布局,作为xml布局的父布局,添加到windowManager中,
+     * 重写自定义布局的touch事件,实现拖拽效果。
+     */
+    private fun addView() {
+        // 创建一个frameLayout作为浮窗布局的父容器
+        frameLayout = ParentFrameLayout(context, config)
+        frameLayout?.tag = config.floatTag
+        // 将浮窗布局文件添加到父容器frameLayout中,并返回该浮窗文件
+        val floatingView =
+            LayoutInflater.from(context).inflate(config.layoutId!!, frameLayout, true)
+        // 为了避免创建的时候闪一下,我们先隐藏视图,不能直接设置GONE,否则定位会出现问题
+        floatingView.visibility = View.INVISIBLE
+        // 将frameLayout添加到系统windowManager中
+        windowManager.addView(frameLayout, params)
+
+        // 通过重写frameLayout的Touch事件,实现拖拽效果
+        frameLayout?.touchListener = object : OnFloatTouchListener {
+            override fun onTouch(event: MotionEvent) =
+                touchUtils.updateFloat(frameLayout!!, event, windowManager, params)
+        }
+
+        // 在浮窗绘制完成的时候,设置初始坐标、执行入场动画
+        frameLayout?.layoutListener = object : ParentFrameLayout.OnLayoutListener {
+            override fun onLayout() {
+                setGravity(frameLayout)
+                config.apply {
+                    // 如果设置了过滤当前页,或者后台显示前台创建、前台显示后台创建,隐藏浮窗,否则执行入场动画
+                    if (filterSelf
+                        || (showPattern == ShowPattern.BACKGROUND && LifecycleUtils.isForeground())
+                        || (showPattern == ShowPattern.FOREGROUND && !LifecycleUtils.isForeground())
+                    ) {
+                        setVisible(View.GONE)
+                        initEditText()
+                    } else enterAnim(floatingView)
+
+                    // 设置callbacks
+                    layoutView = floatingView
+                    invokeView?.invoke(floatingView)
+                    callbacks?.createdResult(true, null, floatingView)
+                    floatCallbacks?.builder?.createdResult?.invoke(true, null, floatingView)
+                }
+            }
+        }
+    }
+
+    private fun initEditText() {
+        if (config.hasEditText) frameLayout?.let { traverseViewGroup(it) }
+    }
+
+    private fun traverseViewGroup(view: View?) {
+        view?.let {
+            // 遍历ViewGroup,是子view判断是否是EditText,是ViewGroup递归调用
+            if (it is ViewGroup) for (i in 0 until it.childCount) {
+                val child = it.getChildAt(i)
+                if (child is ViewGroup) traverseViewGroup(child) else checkEditText(child)
+            } else checkEditText(it)
+        }
+    }
+
+    private fun checkEditText(view: View) {
+        if (view is EditText) InputMethodUtils.initInputMethod(view, config.floatTag)
+    }
+
+
+    /**
+     * 设置浮窗的对齐方式,支持上下左右、居中、上中、下中、左中和右中,默认左上角
+     * 支持手动设置的偏移量
+     */
+    @SuppressLint("RtlHardcoded")
+    private fun setGravity(view: View?) {
+        if (config.locationPair != Pair(0, 0) || view == null) return
+        val parentRect = Rect()
+        // 获取浮窗所在的矩形
+        windowManager.defaultDisplay.getRectSize(parentRect)
+        val location = IntArray(2)
+        // 获取在整个屏幕内的绝对坐标
+        view.getLocationOnScreen(location)
+        // 通过绝对高度和相对高度比较,判断包含顶部状态栏
+        val statusBarHeight = if (location[1] > params.y) DisplayUtils.statusBarHeight(view) else 0
+        val parentBottom =
+            config.displayHeight.getDisplayRealHeight(context) - statusBarHeight
+        when (config.gravity) {
+            // 右上
+            Gravity.END, Gravity.END or Gravity.TOP, Gravity.RIGHT, Gravity.RIGHT or Gravity.TOP ->
+                params.x = parentRect.right - view.width
+            // 左下
+            Gravity.START or Gravity.BOTTOM, Gravity.BOTTOM, Gravity.LEFT or Gravity.BOTTOM ->
+                params.y = parentBottom - view.height
+            // 右下
+            Gravity.END or Gravity.BOTTOM, Gravity.RIGHT or Gravity.BOTTOM -> {
+                params.x = parentRect.right - view.width
+                params.y = parentBottom - view.height
+            }
+            // 居中
+            Gravity.CENTER -> {
+                params.x = (parentRect.right - view.width).shr(1)
+                params.y = (parentBottom - view.height).shr(1)
+            }
+            // 上中
+            Gravity.CENTER_HORIZONTAL, Gravity.TOP or Gravity.CENTER_HORIZONTAL ->
+                params.x = (parentRect.right - view.width).shr(1)
+            // 下中
+            Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL -> {
+                params.x = (parentRect.right - view.width).shr(1)
+                params.y = parentBottom - view.height
+            }
+            // 左中
+            Gravity.CENTER_VERTICAL, Gravity.START or Gravity.CENTER_VERTICAL, Gravity.LEFT or Gravity.CENTER_VERTICAL ->
+                params.y = (parentBottom - view.height).shr(1)
+            // 右中
+            Gravity.END or Gravity.CENTER_VERTICAL, Gravity.RIGHT or Gravity.CENTER_VERTICAL -> {
+                params.x = parentRect.right - view.width
+                params.y = (parentBottom - view.height).shr(1)
+            }
+            // 其他情况,均视为左上
+            else -> {
+            }
+        }
+
+        // 设置偏移量
+        params.x += config.offsetPair.first
+        params.y += config.offsetPair.second
+
+        if (config.immersionStatusBar) {
+            if (config.showPattern != ShowPattern.CURRENT_ACTIVITY) {
+                params.y -= statusBarHeight
+            }
+        } else {
+            if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
+                params.y += statusBarHeight
+            }
+        }
+        // 更新浮窗位置信息
+        windowManager.updateViewLayout(view, params)
+    }
+
+    /**
+     * 设置浮窗的可见性
+     */
+    fun setVisible(visible: Int, needShow: Boolean = true) {
+        if (frameLayout == null || frameLayout!!.childCount < 1) return
+        // 如果用户主动隐藏浮窗,则该值为false
+        config.needShow = needShow
+        frameLayout!!.visibility = visible
+        val view = frameLayout!!.getChildAt(0)
+        if (visible == View.VISIBLE) {
+            config.isShow = true
+            config.callbacks?.show(view)
+            config.floatCallbacks?.builder?.show?.invoke(view)
+        } else {
+            config.isShow = false
+            config.callbacks?.hide(view)
+            config.floatCallbacks?.builder?.hide?.invoke(view)
+        }
+    }
+
+    /**
+     * 入场动画
+     */
+    private fun enterAnim(floatingView: View) {
+        if (frameLayout == null || config.isAnim) return
+        enterAnimator = AnimatorManager(frameLayout!!, params, windowManager, config)
+            .enterAnim()?.apply {
+                // 可以延伸到屏幕外,动画结束按需去除该属性,不然旋转屏幕可能置于屏幕外部
+                params.flags =
+                    FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE or FLAG_LAYOUT_NO_LIMITS
+
+                addListener(object : Animator.AnimatorListener {
+                    override fun onAnimationRepeat(animation: Animator?) {}
+
+                    override fun onAnimationEnd(animation: Animator?) {
+                        config.isAnim = false
+                        if (!config.immersionStatusBar) {
+                            // 不需要延伸到屏幕外了,防止屏幕旋转的时候,浮窗处于屏幕外
+                            params.flags = FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE
+                        }
+                        initEditText()
+                    }
+
+                    override fun onAnimationCancel(animation: Animator?) {}
+
+                    override fun onAnimationStart(animation: Animator?) {
+                        floatingView.visibility = View.VISIBLE
+                        config.isAnim = true
+                    }
+                })
+                start()
+            }
+        if (enterAnimator == null) {
+            floatingView.visibility = View.VISIBLE
+            windowManager.updateViewLayout(floatingView, params)
+        }
+    }
+
+    /**
+     * 退出动画
+     */
+    fun exitAnim() {
+        if (frameLayout == null || (config.isAnim && enterAnimator == null)) return
+        enterAnimator?.cancel()
+        val animator: Animator? =
+            AnimatorManager(frameLayout!!, params, windowManager, config).exitAnim()
+        if (animator == null) remove() else {
+            // 二次判断,防止重复调用引发异常
+            if (config.isAnim) return
+            config.isAnim = true
+            params.flags = FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE or FLAG_LAYOUT_NO_LIMITS
+            animator.addListener(object : Animator.AnimatorListener {
+                override fun onAnimationRepeat(animation: Animator?) {}
+
+                override fun onAnimationEnd(animation: Animator?) = remove()
+
+                override fun onAnimationCancel(animation: Animator?) {}
+
+                override fun onAnimationStart(animation: Animator?) {}
+            })
+            animator.start()
+        }
+    }
+
+    /**
+     * 退出动画执行结束/没有退出动画,进行回调、移除等操作
+     */
+    fun remove(force: Boolean = false) = try {
+        config.isAnim = false
+        FloatingWindowManager.remove(config.floatTag)
+        // removeView是异步删除,在Activity销毁的时候会导致窗口泄漏,所以使用removeViewImmediate直接删除view
+        windowManager.run { if (force) removeViewImmediate(frameLayout) else removeView(frameLayout) }
+    } catch (e: Exception) {
+        Logger.e("浮窗关闭出现异常:$e")
+    }
+
+    /**
+     * 更新浮窗坐标
+     */
+    fun updateFloat(x: Int, y: Int) {
+        frameLayout?.let {
+            if (x == -1 && y == -1) {
+                // 未指定具体坐标,执行吸附动画
+                it.postDelayed({ touchUtils.updateFloat(it, params, windowManager) }, 200)
+            } else {
+                params.x = x
+                params.y = y
+                windowManager.updateViewLayout(it, params)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowManager.kt b/easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowManager.kt
new file mode 100644
index 0000000..1b09b9c
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowManager.kt
@@ -0,0 +1,72 @@
+package com.lzf.easyfloat.core
+
+import android.content.Context
+import android.view.View
+import com.lzf.easyfloat.WARN_REPEATED_TAG
+import com.lzf.easyfloat.data.FloatConfig
+import com.lzf.easyfloat.utils.Logger
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * @author: Liuzhenfeng
+ * @date: 12/1/20  23:36
+ * @Description:
+ */
+internal object FloatingWindowManager {
+
+    private const val DEFAULT_TAG = "default"
+    val windowMap = ConcurrentHashMap<String, FloatingWindowHelper>()
+
+    /**
+     * 创建浮窗,tag不存在创建,tag存在创建失败
+     * 创建结果通过tag添加到相应的map进行管理
+     */
+    fun create(context: Context, config: FloatConfig) = if (!checkTag(config)) {
+        windowMap[config.floatTag!!] =
+            FloatingWindowHelper(context, config).apply { createWindow() }
+    } else {
+        // 存在相同的tag,直接创建失败
+        config.callbacks?.createdResult(false, WARN_REPEATED_TAG, null)
+        Logger.w(WARN_REPEATED_TAG)
+    }
+
+    /**
+     * 关闭浮窗,执行浮窗的退出动画
+     */
+    fun dismiss(tag: String? = null, force: Boolean = false) =
+        getHelper(tag)?.run { if (force) remove(force) else exitAnim() }
+
+    /**
+     * 移除当条浮窗信息,在退出完成后调用
+     */
+    fun remove(floatTag: String?) = windowMap.remove(getTag(floatTag))
+
+    /**
+     * 设置浮窗的显隐,用户主动调用隐藏时,needShow需要为false
+     */
+    fun visible(
+        isShow: Boolean,
+        tag: String? = null,
+        needShow: Boolean = windowMap[tag]?.config?.needShow ?: true
+    ) = getHelper(tag)?.setVisible(if (isShow) View.VISIBLE else View.GONE, needShow)
+
+    /**
+     * 检测浮窗的tag是否有效,不同的浮窗必须设置不同的tag
+     */
+    private fun checkTag(config: FloatConfig): Boolean {
+        // 如果未设置tag,设置默认tag
+        config.floatTag = getTag(config.floatTag)
+        return windowMap.containsKey(config.floatTag!!)
+    }
+
+    /**
+     * 获取浮窗tag,为空则使用默认值
+     */
+    private fun getTag(tag: String?) = tag ?: DEFAULT_TAG
+
+    /**
+     * 获取具体的系统浮窗管理类
+     */
+    fun getHelper(tag: String?) = windowMap[getTag(tag)]
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/core/TouchUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/core/TouchUtils.kt
new file mode 100644
index 0000000..dcb500d
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/core/TouchUtils.kt
@@ -0,0 +1,334 @@
+package com.lzf.easyfloat.core
+
+import android.animation.Animator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams
+import com.lzf.easyfloat.data.FloatConfig
+import com.lzf.easyfloat.enums.ShowPattern
+import com.lzf.easyfloat.enums.SidePattern
+import com.lzf.easyfloat.utils.DisplayUtils
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * @author: liuzhenfeng
+ * @function: 根据吸附模式,实现相应的拖拽效果
+ * @date: 2019-07-05  14:24
+ */
+internal class TouchUtils(val context: Context, val config: FloatConfig) {
+
+    // 窗口所在的矩形
+    private var parentRect: Rect = Rect()
+
+    // 悬浮的父布局高度、宽度
+    private var parentHeight = 0
+    private var parentWidth = 0
+
+    // 四周坐标边界值
+    private var leftBorder = 0
+    private var topBorder = 0
+    private var rightBorder = 0
+    private var bottomBorder = 0
+
+    // 起点坐标
+    private var lastX = 0f
+    private var lastY = 0f
+
+    // 浮窗各边距离父布局的距离
+    private var leftDistance = 0
+    private var rightDistance = 0
+    private var topDistance = 0
+    private var bottomDistance = 0
+
+    // x轴、y轴的最小距离值
+    private var minX = 0
+    private var minY = 0
+    private val location = IntArray(2)
+    private var statusBarHeight = 0
+
+    // 屏幕可用高度 - 浮窗自身高度 的剩余高度
+    private var emptyHeight = 0
+
+    /**
+     * 根据吸附模式,实现相应的拖拽效果
+     */
+    fun updateFloat(
+        view: View,
+        event: MotionEvent,
+        windowManager: WindowManager,
+        params: LayoutParams
+    ) {
+        config.callbacks?.touchEvent(view, event)
+        config.floatCallbacks?.builder?.touchEvent?.invoke(view, event)
+        // 不可拖拽、或者正在执行动画,不做处理
+        if (!config.dragEnable || config.isAnim) {
+            config.isDrag = false
+            return
+        }
+
+        when (event.action and MotionEvent.ACTION_MASK) {
+            MotionEvent.ACTION_DOWN -> {
+                config.isDrag = false
+                // 记录触摸点的位置
+                lastX = event.rawX
+                lastY = event.rawY
+                // 初始化一些边界数据
+                initBoarderValue(view, params)
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                // 过滤边界值之外的拖拽
+                if (event.rawX < leftBorder || event.rawX > rightBorder + view.width
+                    || event.rawY < topBorder || event.rawY > bottomBorder + view.height
+                ) return
+
+                // 移动值 = 本次触摸值 - 上次触摸值
+                val dx = event.rawX - lastX
+                val dy = event.rawY - lastY
+                // 忽略过小的移动,防止点击无效
+                if (!config.isDrag && dx * dx + dy * dy < 81) return
+                config.isDrag = true
+
+                var x = params.x + dx.toInt()
+                var y = params.y + dy.toInt()
+                // 检测浮窗是否到达边缘
+                x = when {
+                    x < leftBorder -> leftBorder
+                    x > rightBorder -> rightBorder
+                    else -> x
+                }
+
+                if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
+                    // 单页面浮窗,设置状态栏不沉浸时,最小高度为状态栏高度
+                    if (y < statusBarHeight(view) && !config.immersionStatusBar) y =
+                        statusBarHeight(view)
+                }
+
+                y = when {
+                    y < topBorder -> topBorder
+                    // 状态栏沉浸时,最小高度为-statusBarHeight,反之最小高度为0
+                    y < 0 -> if (config.immersionStatusBar) {
+                        if (y < -statusBarHeight) -statusBarHeight else y
+                    } else 0
+                    y > bottomBorder -> bottomBorder
+                    else -> y
+                }
+
+                when (config.sidePattern) {
+                    SidePattern.LEFT -> x = 0
+                    SidePattern.RIGHT -> x = parentWidth - view.width
+                    SidePattern.TOP -> y = 0
+                    SidePattern.BOTTOM -> y = emptyHeight
+
+                    SidePattern.AUTO_HORIZONTAL ->
+                        x = if (event.rawX * 2 > parentWidth) parentWidth - view.width else 0
+
+                    SidePattern.AUTO_VERTICAL ->
+                        y = if ((event.rawY - parentRect.top) * 2 > parentHeight)
+                            parentHeight - view.height else 0
+
+                    SidePattern.AUTO_SIDE -> {
+                        leftDistance = event.rawX.toInt()
+                        rightDistance = parentWidth - event.rawX.toInt()
+                        topDistance = event.rawY.toInt() - parentRect.top
+                        bottomDistance = parentHeight + parentRect.top - event.rawY.toInt()
+
+                        minX = min(leftDistance, rightDistance)
+                        minY = min(topDistance, bottomDistance)
+                        if (minX < minY) {
+                            x = if (leftDistance == minX) 0 else parentWidth - view.width
+                        } else {
+                            y = if (topDistance == minY) 0 else emptyHeight
+                        }
+                    }
+                    else -> {
+                    }
+                }
+
+                // 重新设置坐标信息
+                params.x = x
+                params.y = y
+                windowManager.updateViewLayout(view, params)
+                config.callbacks?.drag(view, event)
+                config.floatCallbacks?.builder?.drag?.invoke(view, event)
+                // 更新上次触摸点的数据
+                lastX = event.rawX
+                lastY = event.rawY
+            }
+
+            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+                if (!config.isDrag) return
+                // 回调拖拽事件的ACTION_UP
+                config.callbacks?.drag(view, event)
+                config.floatCallbacks?.builder?.drag?.invoke(view, event)
+                when (config.sidePattern) {
+                    SidePattern.RESULT_LEFT,
+                    SidePattern.RESULT_RIGHT,
+                    SidePattern.RESULT_TOP,
+                    SidePattern.RESULT_BOTTOM,
+                    SidePattern.RESULT_HORIZONTAL,
+                    SidePattern.RESULT_VERTICAL,
+                    SidePattern.RESULT_SIDE -> sideAnim(view, params, windowManager)
+                    else -> {
+                        config.callbacks?.dragEnd(view)
+                        config.floatCallbacks?.builder?.dragEnd?.invoke(view)
+                    }
+                }
+            }
+
+            else -> return
+        }
+    }
+
+    /**
+     * 根据吸附类别,更新浮窗位置
+     */
+    fun updateFloat(
+        view: View,
+        params: LayoutParams,
+        windowManager: WindowManager
+    ) {
+        initBoarderValue(view, params)
+        sideAnim(view, params, windowManager)
+    }
+
+    /**
+     * 初始化边界值等数据
+     */
+    private fun initBoarderValue(view: View, params: LayoutParams) {
+        // 屏幕宽高需要每次获取,可能会有屏幕旋转、虚拟导航栏的状态变化
+        parentWidth = DisplayUtils.getScreenWidth(context)
+        parentHeight = config.displayHeight.getDisplayRealHeight(context)
+        // 获取在整个屏幕内的绝对坐标
+        view.getLocationOnScreen(location)
+        // 通过绝对高度和相对高度比较,判断包含顶部状态栏
+        statusBarHeight = if (location[1] > params.y) statusBarHeight(view) else 0
+        emptyHeight = parentHeight - view.height - statusBarHeight
+
+        leftBorder = max(0, config.leftBorder)
+        rightBorder = min(parentWidth, config.rightBorder) - view.width
+        topBorder = if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
+            // 单页面浮窗,坐标屏幕顶部计算
+            if (config.immersionStatusBar) config.topBorder
+            else config.topBorder + statusBarHeight(view)
+        } else {
+            // 系统浮窗,坐标从状态栏底部开始,沉浸时坐标为负
+            if (config.immersionStatusBar) config.topBorder - statusBarHeight(view) else config.topBorder
+        }
+        bottomBorder = if (config.showPattern == ShowPattern.CURRENT_ACTIVITY) {
+            // 单页面浮窗,坐标屏幕顶部计算
+            if (config.immersionStatusBar)
+                min(emptyHeight, config.bottomBorder - view.height)
+            else
+                min(emptyHeight, config.bottomBorder + statusBarHeight(view) - view.height)
+        } else {
+            // 系统浮窗,坐标从状态栏底部开始,沉浸时坐标为负
+            if (config.immersionStatusBar)
+                min(emptyHeight, config.bottomBorder - statusBarHeight(view) - view.height)
+            else
+                min(emptyHeight, config.bottomBorder - view.height)
+        }
+    }
+
+    private fun sideAnim(
+        view: View,
+        params: LayoutParams,
+        windowManager: WindowManager
+    ) {
+        initDistanceValue(params)
+        val isX: Boolean
+        val end = when (config.sidePattern) {
+            SidePattern.RESULT_LEFT -> {
+                isX = true
+                leftBorder
+            }
+            SidePattern.RESULT_RIGHT -> {
+                isX = true
+                params.x + rightDistance
+            }
+            SidePattern.RESULT_HORIZONTAL -> {
+                isX = true
+                if (leftDistance < rightDistance) leftBorder else params.x + rightDistance
+            }
+
+            SidePattern.RESULT_TOP -> {
+                isX = false
+                topBorder
+            }
+            SidePattern.RESULT_BOTTOM -> {
+                isX = false
+                // 不要轻易使用此相关模式,需要考虑虚拟导航栏的情况
+                bottomBorder
+            }
+            SidePattern.RESULT_VERTICAL -> {
+                isX = false
+                if (topDistance < bottomDistance) topBorder else bottomBorder
+            }
+
+            SidePattern.RESULT_SIDE -> {
+                if (minX < minY) {
+                    isX = true
+                    if (leftDistance < rightDistance) leftBorder else params.x + rightDistance
+                } else {
+                    isX = false
+                    if (topDistance < bottomDistance) topBorder else bottomBorder
+                }
+            }
+            else -> return
+        }
+
+        val animator = ValueAnimator.ofInt(if (isX) params.x else params.y, end)
+        animator.addUpdateListener {
+            try {
+                if (isX) params.x = it.animatedValue as Int else params.y = it.animatedValue as Int
+                // 极端情况,还没吸附就调用了关闭浮窗,会导致吸附闪退
+                windowManager.updateViewLayout(view, params)
+            } catch (e: Exception) {
+                animator.cancel()
+            }
+        }
+        animator.addListener(object : Animator.AnimatorListener {
+            override fun onAnimationRepeat(animation: Animator?) {}
+
+            override fun onAnimationEnd(animation: Animator?) {
+                dragEnd(view)
+            }
+
+            override fun onAnimationCancel(animation: Animator?) {
+                dragEnd(view)
+            }
+
+            override fun onAnimationStart(animation: Animator?) {
+                config.isAnim = true
+            }
+        })
+        animator.start()
+    }
+
+    private fun dragEnd(view: View) {
+        config.isAnim = false
+        config.callbacks?.dragEnd(view)
+        config.floatCallbacks?.builder?.dragEnd?.invoke(view)
+    }
+
+    /**
+     * 计算一些边界距离数据
+     */
+    private fun initDistanceValue(params: LayoutParams) {
+        leftDistance = params.x - leftBorder
+        rightDistance = rightBorder - params.x
+        topDistance = params.y - topBorder
+        bottomDistance = bottomBorder - params.y
+
+        minX = min(leftDistance, rightDistance)
+        minY = min(topDistance, bottomDistance)
+    }
+
+    private fun statusBarHeight(view: View) = DisplayUtils.statusBarHeight(view)
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/data/FloatConfig.kt b/easyfloat/src/main/java/com/lzf/easyfloat/data/FloatConfig.kt
new file mode 100644
index 0000000..0392856
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/data/FloatConfig.kt
@@ -0,0 +1,80 @@
+package com.lzf.easyfloat.data
+
+import android.view.View
+import com.lzf.easyfloat.anim.DefaultAnimator
+import com.lzf.easyfloat.enums.ShowPattern
+import com.lzf.easyfloat.enums.SidePattern
+import com.lzf.easyfloat.interfaces.*
+import com.lzf.easyfloat.utils.DefaultDisplayHeight
+
+/**
+ * @author: liuzhenfeng
+ * @function: 浮窗的数据类,方便管理各属性
+ * @date: 2019-07-29  10:14
+ */
+data class FloatConfig(
+
+    // 浮窗的xml布局文件
+    var layoutId: Int? = null,
+    var layoutView: View? = null,
+
+    // 当前浮窗的tag
+    var floatTag: String? = null,
+
+    // 是否可拖拽
+    var dragEnable: Boolean = true,
+    // 是否正在被拖拽
+    var isDrag: Boolean = false,
+    // 是否正在执行动画
+    var isAnim: Boolean = false,
+    // 是否显示
+    var isShow: Boolean = false,
+    // 是否包含EditText
+    var hasEditText: Boolean = false,
+    // 状态栏沉浸
+    var immersionStatusBar: Boolean = false,
+
+    // 浮窗的吸附方式(默认不吸附,拖到哪里是哪里)
+    var sidePattern: SidePattern = SidePattern.DEFAULT,
+
+    // 浮窗显示类型(默认只在当前页显示)
+    var showPattern: ShowPattern = ShowPattern.CURRENT_ACTIVITY,
+
+    // 宽高是否充满父布局
+    var widthMatch: Boolean = false,
+    var heightMatch: Boolean = false,
+
+    // 浮窗的摆放方式,使用系统的Gravity属性
+    var gravity: Int = 0,
+    // 坐标的偏移量
+    var offsetPair: Pair<Int, Int> = Pair(0, 0),
+    // 固定的初始坐标,左上角坐标
+    var locationPair: Pair<Int, Int> = Pair(0, 0),
+    // ps:优先使用固定坐标,若固定坐标不为原点坐标,gravity属性和offset属性无效
+
+    // 四周边界值
+    var leftBorder: Int = 0,
+    var topBorder: Int = -999,
+    var rightBorder: Int = 9999,
+    var bottomBorder: Int = 9999,
+
+    // Callbacks
+    var invokeView: OnInvokeView? = null,
+    var callbacks: OnFloatCallbacks? = null,
+    // 通过Kotlin DSL设置回调,无需复写全部方法,按需复写
+    var floatCallbacks: FloatCallbacks? = null,
+
+    // 出入动画
+    var floatAnimator: OnFloatAnimator? = DefaultAnimator(),
+
+    // 设置屏幕的有效显示高度(不包含虚拟导航栏的高度),仅针对系统浮窗,一般不用复写
+    var displayHeight: OnDisplayHeight = DefaultDisplayHeight(),
+
+    // 不需要显示系统浮窗的页面集合,参数为类名
+    val filterSet: MutableSet<String> = mutableSetOf(),
+    // 是否设置,当前创建的页面也被过滤
+    internal var filterSelf: Boolean = false,
+    // 是否需要显示,当过滤信息匹配上时,该值为false(用户手动调用隐藏,该值也为false,相当于手动过滤)
+    internal var needShow: Boolean = true
+
+)
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/enums/ShowPattern.kt b/easyfloat/src/main/java/com/lzf/easyfloat/enums/ShowPattern.kt
new file mode 100644
index 0000000..ddea8e1
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/enums/ShowPattern.kt
@@ -0,0 +1,12 @@
+package com.lzf.easyfloat.enums
+
+/**
+ * @author: liuzhenfeng
+ * @function: 浮窗显示类别
+ * @date: 2019-07-08  17:05
+ */
+enum class ShowPattern {
+
+    // 只在当前Activity显示、仅应用前台时显示、仅应用后台时显示,一直显示(不分前后台)
+    CURRENT_ACTIVITY, FOREGROUND, BACKGROUND, ALL_TIME
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/enums/SidePattern.kt b/easyfloat/src/main/java/com/lzf/easyfloat/enums/SidePattern.kt
new file mode 100644
index 0000000..7b0f965
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/enums/SidePattern.kt
@@ -0,0 +1,20 @@
+package com.lzf.easyfloat.enums
+
+/**
+ * @author: liuzhenfeng
+ * @function: 浮窗的贴边模式
+ * @date: 2019-07-01  17:34
+ */
+enum class SidePattern {
+
+    // 默认不贴边,跟随手指移动
+    DEFAULT,
+    // 左、右、上、下四个方向固定(一直吸附在该方向边缘,只能在该方向的边缘移动)
+    LEFT, RIGHT, TOP, BOTTOM,
+    // 根据手指到屏幕边缘的距离,自动选择水平方向的贴边、垂直方向的贴边、四周方向的贴边
+    AUTO_HORIZONTAL, AUTO_VERTICAL, AUTO_SIDE,
+    // 拖拽时跟随手指移动,结束时贴边
+    RESULT_LEFT, RESULT_RIGHT, RESULT_TOP, RESULT_BOTTOM,
+    RESULT_HORIZONTAL, RESULT_VERTICAL, RESULT_SIDE
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/FloatCallbacks.kt b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/FloatCallbacks.kt
new file mode 100644
index 0000000..0b6d82b
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/FloatCallbacks.kt
@@ -0,0 +1,58 @@
+package com.lzf.easyfloat.interfaces
+
+import android.view.MotionEvent
+import android.view.View
+
+/**
+ * @author: liuzhenfeng
+ * @function: 通过Kotlin DSL实现接口回调效果
+ * @date: 2019-08-26  17:06
+ */
+class FloatCallbacks {
+
+    lateinit var builder: Builder
+
+    // 带Builder返回值的lambda
+    fun registerListener(builder: Builder.() -> Unit) {
+        this.builder = Builder().also(builder)
+    }
+
+    inner class Builder {
+        internal var createdResult: ((Boolean, String?, View?) -> Unit)? = null
+        internal var show: ((View) -> Unit)? = null
+        internal var hide: ((View) -> Unit)? = null
+        internal var dismiss: (() -> Unit)? = null
+        internal var touchEvent: ((View, MotionEvent) -> Unit)? = null
+        internal var drag: ((View, MotionEvent) -> Unit)? = null
+        internal var dragEnd: ((View) -> Unit)? = null
+
+        fun createResult(action: (Boolean, String?, View?) -> Unit) {
+            createdResult = action
+        }
+
+        fun show(action: (View) -> Unit) {
+            show = action
+        }
+
+        fun hide(action: (View) -> Unit) {
+            hide = action
+        }
+
+        fun dismiss(action: () -> Unit) {
+            dismiss = action
+        }
+
+        fun touchEvent(action: (View, MotionEvent) -> Unit) {
+            touchEvent = action
+        }
+
+        fun drag(action: (View, MotionEvent) -> Unit) {
+            drag = action
+        }
+
+        fun dragEnd(action: (View) -> Unit) {
+            dragEnd = action
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnDisplayHeight.java b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnDisplayHeight.java
new file mode 100644
index 0000000..627faa7
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnDisplayHeight.java
@@ -0,0 +1,21 @@
+package com.lzf.easyfloat.interfaces;
+
+import android.content.Context;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author: liuzhenfeng
+ * @function: 通过接口获取屏幕的有效显示高度
+ * @date: 2020-02-16  16:21
+ */
+public interface OnDisplayHeight {
+
+    /**
+     * 获取屏幕有效的显示高度,不包含虚拟导航栏
+     *
+     * @param context ApplicationContext
+     * @return 高度值(int类型)
+     */
+    int getDisplayRealHeight(@NotNull Context context);
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatAnimator.kt b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatAnimator.kt
new file mode 100644
index 0000000..f4e3150
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatAnimator.kt
@@ -0,0 +1,29 @@
+package com.lzf.easyfloat.interfaces
+
+import android.animation.Animator
+import android.view.View
+import android.view.WindowManager
+import com.lzf.easyfloat.enums.SidePattern
+
+/**
+ * @author: liuzhenfeng
+ * @function: 系统浮窗的出入动画
+ * @date: 2019-07-22  16:40
+ */
+interface OnFloatAnimator {
+
+    fun enterAnim(
+        view: View,
+        params: WindowManager.LayoutParams,
+        windowManager: WindowManager,
+        sidePattern: SidePattern
+    ): Animator? = null
+
+    fun exitAnim(
+        view: View,
+        params: WindowManager.LayoutParams,
+        windowManager: WindowManager,
+        sidePattern: SidePattern
+    ): Animator? = null
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatCallbacks.kt b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatCallbacks.kt
new file mode 100644
index 0000000..dc66f62
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatCallbacks.kt
@@ -0,0 +1,43 @@
+package com.lzf.easyfloat.interfaces
+
+import android.view.MotionEvent
+import android.view.View
+
+/**
+ * @author: liuzhenfeng
+ * @function: 浮窗的一些状态回调
+ * @date: 2019-07-16  14:11
+ */
+interface OnFloatCallbacks {
+
+    /**
+     * 浮窗的创建结果,是否创建成功
+     *
+     * @param isCreated     是否创建成功
+     * @param msg           失败返回的结果
+     * @param view          浮窗xml布局
+     */
+    fun createdResult(isCreated: Boolean, msg: String?, view: View?)
+
+    fun show(view: View)
+
+    fun hide(view: View)
+
+    fun dismiss()
+
+    /**
+     * 触摸事件的回调
+     */
+    fun touchEvent(view: View, event: MotionEvent)
+
+    /**
+     * 浮窗被拖拽时的回调,坐标为浮窗的左上角坐标
+     */
+    fun drag(view: View, event: MotionEvent)
+
+    /**
+     * 拖拽结束时的回调,坐标为浮窗的左上角坐标
+     */
+    fun dragEnd(view: View)
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatTouchListener.kt b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatTouchListener.kt
new file mode 100644
index 0000000..0aca1ba
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatTouchListener.kt
@@ -0,0 +1,13 @@
+package com.lzf.easyfloat.interfaces
+
+import android.view.MotionEvent
+
+/**
+ * @author: liuzhenfeng
+ * @function: 系统浮窗的触摸事件
+ * @date: 2019-07-11  11:06
+ */
+internal interface OnFloatTouchListener {
+
+    fun onTouch(event: MotionEvent)
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnInvokeView.java b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnInvokeView.java
new file mode 100644
index 0000000..5ad4cc0
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnInvokeView.java
@@ -0,0 +1,18 @@
+package com.lzf.easyfloat.interfaces;
+
+import android.view.View;
+
+/**
+ * @author: liuzhenfeng
+ * @function: 设置浮窗内容的接口,由于kotlin暂不支持SAM,所以使用Java接口
+ * @date: 2019-06-30  14:19
+ */
+public interface OnInvokeView {
+
+    /**
+     * 设置浮窗布局的具体内容
+     *
+     * @param view 浮窗布局
+     */
+    void invoke(View view);
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnPermissionResult.kt b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnPermissionResult.kt
new file mode 100644
index 0000000..f5b1804
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnPermissionResult.kt
@@ -0,0 +1,11 @@
+package com.lzf.easyfloat.interfaces
+
+/**
+ * @author: liuzhenfeng
+ * @function: 浮窗权限的申请结果
+ * @date: 2019-07-15  16:18
+ */
+interface OnPermissionResult {
+
+    fun permissionResult(isOpen: Boolean)
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnTouchRangeListener.kt b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnTouchRangeListener.kt
new file mode 100644
index 0000000..b485d8a
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnTouchRangeListener.kt
@@ -0,0 +1,23 @@
+package com.lzf.easyfloat.interfaces
+
+import com.lzf.easyfloat.widget.BaseSwitchView
+
+/**
+ * @author: liuzhenfeng
+ * @date: 2020/10/25  20:25
+ * @Package: com.lzf.easyfloat.interfaces
+ * @Description: 区域触摸事件回调
+ */
+interface OnTouchRangeListener {
+
+    /**
+     * 手指触摸到指定区域
+     */
+    fun touchInRange(inRange: Boolean, view: BaseSwitchView)
+
+    /**
+     * 在指定区域抬起手指
+     */
+    fun touchUpInRange()
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionFragment.kt b/easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionFragment.kt
new file mode 100644
index 0000000..7494225
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionFragment.kt
@@ -0,0 +1,54 @@
+package com.lzf.easyfloat.permission
+
+import android.app.Activity
+import android.app.Fragment
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import com.lzf.easyfloat.interfaces.OnPermissionResult
+import com.lzf.easyfloat.utils.Logger
+
+/**
+ * @author: liuzhenfeng
+ * @function: 用于浮窗权限的申请,自动处理回调结果
+ * @date: 2019-07-15  10:36
+ */
+internal class PermissionFragment : Fragment() {
+
+    companion object {
+        private var onPermissionResult: OnPermissionResult? = null
+
+        fun requestPermission(activity: Activity, onPermissionResult: OnPermissionResult) {
+            this.onPermissionResult = onPermissionResult
+            activity.fragmentManager
+                .beginTransaction()
+                .add(PermissionFragment(), activity.localClassName)
+                .commitAllowingStateLoss()
+        }
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+        // 权限申请
+        PermissionUtils.requestPermission(this)
+        Logger.i("PermissionFragment:requestPermission")
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == PermissionUtils.requestCode) {
+            // 需要延迟执行,不然即使授权,仍有部分机型获取不到权限
+            Handler(Looper.getMainLooper()).postDelayed({
+                val activity = activity ?: return@postDelayed
+                val check = PermissionUtils.checkPermission(activity)
+                Logger.i("PermissionFragment onActivityResult: $check")
+                // 回调权限结果
+                onPermissionResult?.permissionResult(check)
+                onPermissionResult = null
+                // 将Fragment移除
+                fragmentManager.beginTransaction().remove(this).commitAllowingStateLoss()
+            }, 500)
+        }
+    }
+
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionUtils.kt
new file mode 100644
index 0000000..a904c44
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionUtils.kt
@@ -0,0 +1,117 @@
+package com.lzf.easyfloat.permission
+
+import android.app.Activity
+import android.app.Fragment
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.util.Log
+import com.lzf.easyfloat.interfaces.OnPermissionResult
+import com.lzf.easyfloat.permission.rom.*
+import com.lzf.easyfloat.utils.Logger
+
+/**
+ * @author: liuzhenfeng
+ * @function: 悬浮窗权限工具类
+ * @date: 2019-07-15  10:22
+ */
+object PermissionUtils {
+
+    internal const val requestCode = 199
+    private const val TAG = "PermissionUtils--->"
+
+    /**
+     * 检测是否有悬浮窗权限
+     * 6.0 版本之后由于 google 增加了对悬浮窗权限的管理,所以方式就统一了
+     */
+    @JvmStatic
+    fun checkPermission(context: Context): Boolean =
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) when {
+            RomUtils.checkIsHuaweiRom() -> huaweiPermissionCheck(context)
+            RomUtils.checkIsMiuiRom() -> miuiPermissionCheck(context)
+            RomUtils.checkIsOppoRom() -> oppoROMPermissionCheck(context)
+            RomUtils.checkIsMeizuRom() -> meizuPermissionCheck(context)
+            RomUtils.checkIs360Rom() -> qikuPermissionCheck(context)
+            else -> true
+        } else commonROMPermissionCheck(context)
+
+    /**
+     * 申请悬浮窗权限
+     */
+    @JvmStatic
+    fun requestPermission(activity: Activity, onPermissionResult: OnPermissionResult) =
+        PermissionFragment.requestPermission(activity, onPermissionResult)
+
+    internal fun requestPermission(fragment: Fragment) =
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) when {
+            RomUtils.checkIsHuaweiRom() -> HuaweiUtils.applyPermission(fragment)
+            RomUtils.checkIsMiuiRom() -> MiuiUtils.applyMiuiPermission(fragment)
+            RomUtils.checkIsOppoRom() -> OppoUtils.applyOppoPermission(fragment)
+            RomUtils.checkIsMeizuRom() -> MeizuUtils.applyPermission(fragment)
+            RomUtils.checkIs360Rom() -> QikuUtils.applyPermission(fragment)
+            else -> Logger.i(TAG, "原生 Android 6.0 以下无需权限申请")
+        } else commonROMPermissionApply(fragment)
+
+    private fun huaweiPermissionCheck(context: Context) =
+        HuaweiUtils.checkFloatWindowPermission(context)
+
+    private fun miuiPermissionCheck(context: Context) =
+        MiuiUtils.checkFloatWindowPermission(context)
+
+    private fun meizuPermissionCheck(context: Context) =
+        MeizuUtils.checkFloatWindowPermission(context)
+
+    private fun qikuPermissionCheck(context: Context) =
+        QikuUtils.checkFloatWindowPermission(context)
+
+    private fun oppoROMPermissionCheck(context: Context) =
+        OppoUtils.checkFloatWindowPermission(context)
+
+    /**
+     * 6.0以后,通用悬浮窗权限检测
+     * 但是魅族6.0的系统这种方式不好用,需要单独适配一下
+     */
+    private fun commonROMPermissionCheck(context: Context): Boolean =
+        if (RomUtils.checkIsMeizuRom()) meizuPermissionCheck(context) else {
+            var result = true
+            if (Build.VERSION.SDK_INT >= 23) try {
+                val clazz = Settings::class.java
+                val canDrawOverlays =
+                    clazz.getDeclaredMethod("canDrawOverlays", Context::class.java)
+                result = canDrawOverlays.invoke(null, context) as Boolean
+            } catch (e: Exception) {
+                Log.e(TAG, Log.getStackTraceString(e))
+            }
+            result
+        }
+
+    /**
+     * 通用 rom 权限申请
+     */
+    private fun commonROMPermissionApply(fragment: Fragment) = when {
+        // 这里也一样,魅族系统需要单独适配
+        RomUtils.checkIsMeizuRom() -> MeizuUtils.applyPermission(fragment)
+        Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> try {
+            commonROMPermissionApplyInternal(fragment)
+        } catch (e: Exception) {
+            Logger.e(TAG, Log.getStackTraceString(e))
+        }
+        // 需要做统计效果
+        else -> Logger.d(TAG, "user manually refuse OVERLAY_PERMISSION")
+    }
+
+    @JvmStatic
+    fun commonROMPermissionApplyInternal(fragment: Fragment) = try {
+        val clazz = Settings::class.java
+        val field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION")
+        val intent = Intent(field.get(null).toString())
+        intent.data = Uri.parse("package:${fragment.activity.packageName}")
+        fragment.startActivityForResult(intent, requestCode)
+    } catch (e: Exception) {
+        Logger.e(TAG, "$e")
+    }
+
+}
+
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/HuaweiUtils.java b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/HuaweiUtils.java
new file mode 100644
index 0000000..a696a58
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/HuaweiUtils.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved.
+ */
+package com.lzf.easyfloat.permission.rom;
+
+import android.annotation.TargetApi;
+import android.app.AppOpsManager;
+import android.app.Fragment;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.lzf.easyfloat.permission.PermissionUtils;
+
+import java.lang.reflect.Method;
+
+public class HuaweiUtils {
+    private static final String TAG = "HuaweiUtils";
+
+    /**
+     * 检测 Huawei 悬浮窗权限
+     */
+    public static boolean checkFloatWindowPermission(Context context) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24;
+        }
+        return true;
+    }
+
+    /**
+     * 去华为权限申请页面
+     */
+    public static void applyPermission(Fragment fragment) {
+        try {
+            Intent intent = new Intent();
+            //华为权限管理,跳转到指定app的权限管理位置需要华为接口权限,未解决
+            ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity");//悬浮窗管理页面
+            intent.setComponent(comp);
+            if (RomUtils.getEmuiVersion() == 3.1) {
+                //emui 3.1 的适配
+                fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+            } else {
+                //emui 3.0 的适配
+                comp = new ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity");//悬浮窗管理页面
+                intent.setComponent(comp);
+                fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+            }
+        } catch (SecurityException e) {
+            Intent intent = new Intent();
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            //华为权限管理
+            ComponentName comp = new ComponentName("com.huawei.systemmanager",
+                    "com.huawei.permissionmanager.ui.MainActivity");
+            //华为权限管理,跳转到本app的权限管理页面,这个需要华为接口权限,未解决
+            // 悬浮窗管理页面
+            intent.setComponent(comp);
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+            Log.e(TAG, Log.getStackTraceString(e));
+        } catch (ActivityNotFoundException e) {
+            /**
+             * 手机管家版本较低 HUAWEI SC-UL10
+             */
+            Intent intent = new Intent();
+            //权限管理页面 android4.4
+            ComponentName comp = new ComponentName("com.Android.settings", "com.android.settings.permission.TabItem");
+            //此处可跳转到指定app对应的权限管理页面,但是需要相关权限,未解决
+            intent.setComponent(comp);
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+            e.printStackTrace();
+            Log.e(TAG, Log.getStackTraceString(e));
+        } catch (Exception e) {
+            //抛出异常时提示信息
+            Toast.makeText(fragment.getActivity(), "进入设置页面失败,请手动设置", Toast.LENGTH_LONG).show();
+            Log.e(TAG, Log.getStackTraceString(e));
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static boolean checkOp(Context context, int op) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Class clazz = AppOpsManager.class;
+                Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        } else {
+            Log.e(TAG, "Below API 19 cannot invoke!");
+        }
+        return false;
+    }
+}
+
+
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MeizuUtils.java b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MeizuUtils.java
new file mode 100644
index 0000000..b8702c1
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MeizuUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved.
+ */
+package com.lzf.easyfloat.permission.rom;
+
+import android.annotation.TargetApi;
+import android.app.AppOpsManager;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.util.Log;
+
+import com.lzf.easyfloat.permission.PermissionUtils;
+
+import java.lang.reflect.Method;
+
+
+public class MeizuUtils {
+    private static final String TAG = "MeizuUtils";
+
+    /**
+     * 检测 meizu 悬浮窗权限
+     */
+    public static boolean checkFloatWindowPermission(Context context) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            // OP_SYSTEM_ALERT_WINDOW = 24;
+            return checkOp(context, 24);
+        }
+        return true;
+    }
+
+    /**
+     * 去魅族权限申请页面
+     */
+    public static void applyPermission(Fragment fragment) {
+        try {
+            Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
+            intent.putExtra("packageName", fragment.getActivity().getPackageName());
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } catch (Exception e) {
+            try {
+                Log.e(TAG, "获取悬浮窗权限, 打开AppSecActivity失败, " + Log.getStackTraceString(e));
+                // 最新的魅族flyme 6.2.5 用上述方法获取权限失败, 不过又可以用下述方法获取权限了
+                PermissionUtils.commonROMPermissionApplyInternal(fragment);
+            } catch (Exception eFinal) {
+                Log.e(TAG, "获取悬浮窗权限失败, 通用获取方法失败, " + Log.getStackTraceString(eFinal));
+            }
+        }
+
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static boolean checkOp(Context context, int op) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Class clazz = AppOpsManager.class;
+                Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        } else {
+            Log.e(TAG, "Below API 19 cannot invoke!");
+        }
+        return false;
+    }
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MiuiUtils.java b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MiuiUtils.java
new file mode 100644
index 0000000..3578d29
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MiuiUtils.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved.
+ */
+package com.lzf.easyfloat.permission.rom;
+
+import android.annotation.TargetApi;
+import android.app.AppOpsManager;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.lzf.easyfloat.permission.PermissionUtils;
+
+import java.lang.reflect.Method;
+
+public class MiuiUtils {
+    private static final String TAG = "MiuiUtils";
+
+    /**
+     * 获取小米 rom 版本号,获取失败返回 -1
+     *
+     * @return miui rom version code, if fail , return -1
+     */
+    public static int getMiuiVersion() {
+        String version = RomUtils.getSystemProperty("ro.miui.ui.version.name");
+        if (version != null) {
+            try {
+                return Integer.parseInt(version.substring(1));
+            } catch (Exception e) {
+                Log.e(TAG, "get miui version code error, version : " + version);
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * 检测 miui 悬浮窗权限
+     */
+    public static boolean checkFloatWindowPermission(Context context) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24;
+        } else {
+            return true;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static boolean checkOp(Context context, int op) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Class clazz = AppOpsManager.class;
+                Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        } else {
+            Log.e(TAG, "Below API 19 cannot invoke!");
+        }
+        return false;
+    }
+
+    /**
+     * 小米 ROM 权限申请
+     */
+    public static void applyMiuiPermission(Fragment fragment) {
+        int versionCode = getMiuiVersion();
+        if (versionCode == 5) {
+            goToMiuiPermissionActivity_V5(fragment);
+        } else if (versionCode == 6) {
+            goToMiuiPermissionActivity_V6(fragment);
+        } else if (versionCode == 7) {
+            goToMiuiPermissionActivity_V7(fragment);
+        } else if (versionCode >= 8) {
+            goToMiuiPermissionActivity_V8(fragment);
+        } else {
+            Log.e(TAG, "this is a special MIUI rom version, its version code " + versionCode);
+        }
+    }
+
+    private static boolean isIntentAvailable(Intent intent, Context context) {
+        if (intent == null) {
+            return false;
+        }
+        return context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
+    }
+
+    /**
+     * 小米 V5 版本 ROM权限申请
+     */
+    public static void goToMiuiPermissionActivity_V5(Fragment fragment) {
+        String packageName = fragment.getActivity().getPackageName();
+        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+        Uri uri = Uri.fromParts("package", packageName, null);
+        intent.setData(uri);
+        if (isIntentAvailable(intent, fragment.getActivity())) {
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } else {
+            Log.e(TAG, "intent is not available!");
+        }
+    }
+
+    /**
+     * 小米 V6 版本 ROM权限申请
+     */
+    public static void goToMiuiPermissionActivity_V6(Fragment fragment) {
+        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
+        intent.putExtra("extra_pkgname", fragment.getActivity().getPackageName());
+        if (isIntentAvailable(intent, fragment.getActivity())) {
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } else {
+            Log.e(TAG, "Intent is not available!");
+        }
+    }
+
+    /**
+     * 小米 V7 版本 ROM权限申请
+     */
+    public static void goToMiuiPermissionActivity_V7(Fragment fragment) {
+        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
+        intent.putExtra("extra_pkgname", fragment.getActivity().getPackageName());
+        if (isIntentAvailable(intent, fragment.getActivity())) {
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } else {
+            Log.e(TAG, "Intent is not available!");
+        }
+    }
+
+    /**
+     * 小米 V8 版本 ROM权限申请
+     */
+    public static void goToMiuiPermissionActivity_V8(Fragment fragment) {
+        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
+        intent.putExtra("extra_pkgname", fragment.getActivity().getPackageName());
+        if (isIntentAvailable(intent, fragment.getActivity())) {
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } else {
+            intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
+            intent.setPackage("com.miui.securitycenter");
+            intent.putExtra("extra_pkgname", fragment.getActivity().getPackageName());
+            if (isIntentAvailable(intent, fragment.getActivity())) {
+                fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+            } else {
+                Log.e(TAG, "Intent is not available!");
+            }
+        }
+    }
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/OppoUtils.java b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/OppoUtils.java
new file mode 100644
index 0000000..1238a28
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/OppoUtils.java
@@ -0,0 +1,72 @@
+package com.lzf.easyfloat.permission.rom;
+
+import android.annotation.TargetApi;
+import android.app.AppOpsManager;
+import android.app.Fragment;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.util.Log;
+
+import com.lzf.easyfloat.permission.PermissionUtils;
+
+import java.lang.reflect.Method;
+
+/**
+ * Description:
+ *
+ * @author Shawn_Dut
+ * @since 2018-02-01
+ */
+public class OppoUtils {
+
+    private static final String TAG = "OppoUtils";
+
+    /**
+     * 检测 360 悬浮窗权限
+     */
+    public static boolean checkFloatWindowPermission(Context context) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            // OP_SYSTEM_ALERT_WINDOW = 24;
+            return checkOp(context, 24);
+        }
+        return true;
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static boolean checkOp(Context context, int op) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Class clazz = AppOpsManager.class;
+                Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        } else {
+            Log.e(TAG, "Below API 19 cannot invoke!");
+        }
+        return false;
+    }
+
+    /**
+     * oppo ROM 权限申请
+     */
+    public static void applyOppoPermission(Fragment fragment) {
+        //merge requestPermission from https://github.com/zhaozepeng/FloatWindowPermission/pull/26
+        try {
+            Intent intent = new Intent();
+            //悬浮窗管理页面
+            ComponentName comp = new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity");
+            intent.setComponent(comp);
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/QikuUtils.java b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/QikuUtils.java
new file mode 100644
index 0000000..8406a24
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/QikuUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved.
+ */
+package com.lzf.easyfloat.permission.rom;
+
+import android.annotation.TargetApi;
+import android.app.AppOpsManager;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Build;
+import android.util.Log;
+
+import com.lzf.easyfloat.permission.PermissionUtils;
+
+import java.lang.reflect.Method;
+
+public class QikuUtils {
+    private static final String TAG = "QikuUtils";
+
+    /**
+     * 检测 360 悬浮窗权限
+     */
+    public static boolean checkFloatWindowPermission(Context context) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            // OP_SYSTEM_ALERT_WINDOW = 24;
+            return checkOp(context, 24);
+        }
+        return true;
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static boolean checkOp(Context context, int op) {
+        final int version = Build.VERSION.SDK_INT;
+        if (version >= 19) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Class clazz = AppOpsManager.class;
+                Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        } else {
+            Log.e("", "Below API 19 cannot invoke!");
+        }
+        return false;
+    }
+
+    /**
+     * 去360权限申请页面
+     */
+    public static void applyPermission(Fragment fragment) {
+        Intent intent = new Intent();
+        intent.setClassName("com.android.settings", "com.android.settings.Settings$OverlaySettingsActivity");
+        if (isIntentAvailable(intent, fragment.getActivity())) {
+            fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+        } else {
+            intent.setClassName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
+            if (isIntentAvailable(intent, fragment.getActivity())) {
+                fragment.startActivityForResult(intent, PermissionUtils.requestCode);
+            } else {
+                Log.e(TAG, "can't open permission page with particular name, please use " +
+                        "\"adb shell dumpsys activity\" command and tell me the name of the float window permission page");
+            }
+        }
+    }
+
+    private static boolean isIntentAvailable(Intent intent, Context context) {
+        if (intent == null) {
+            return false;
+        }
+        return context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
+    }
+}
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/RomUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/RomUtils.kt
new file mode 100644
index 0000000..9ca218d
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/RomUtils.kt
@@ -0,0 +1,77 @@
+package com.lzf.easyfloat.permission.rom
+
+import android.os.Build
+import android.text.TextUtils
+import android.util.Log
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStreamReader
+
+/**
+ * @author: liuzhenfeng
+ * @github:https://github.com/princekin-f/EasyFloat
+ * @function: 判断手机ROM
+ * @date: 2020-01-07  22:30
+ */
+object RomUtils {
+    private const val TAG = "RomUtils--->"
+
+    /**
+     * 获取 emui 版本号
+     */
+    @JvmStatic
+    fun getEmuiVersion(): Double {
+        try {
+            val emuiVersion = getSystemProperty("ro.build.version.emui")
+            val version = emuiVersion!!.substring(emuiVersion.indexOf("_") + 1)
+            return version.toDouble()
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+        return 4.0
+    }
+
+    @JvmStatic
+    fun getSystemProperty(propName: String): String? {
+        val line: String
+        var input: BufferedReader? = null
+        try {
+            val p = Runtime.getRuntime().exec("getprop $propName")
+            input = BufferedReader(InputStreamReader(p.inputStream), 1024)
+            line = input.readLine()
+            input.close()
+        } catch (ex: Exception) {
+            Log.e(TAG, "Unable to read sysprop $propName", ex)
+            return null
+        } finally {
+            if (input != null) {
+                try {
+                    input.close()
+                } catch (e: IOException) {
+                    Log.e(TAG, "Exception while closing InputStream", e)
+                }
+            }
+        }
+        return line
+    }
+
+    fun checkIsHuaweiRom() = Build.MANUFACTURER.contains("HUAWEI")
+
+    fun checkIsMiuiRom() = !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.name"))
+
+    fun checkIsMeizuRom(): Boolean {
+        val systemProperty = getSystemProperty("ro.build.display.id")
+        return if (TextUtils.isEmpty(systemProperty)) false
+        else systemProperty!!.contains("flyme") || systemProperty.toLowerCase().contains("flyme")
+    }
+
+    fun checkIs360Rom(): Boolean =
+        Build.MANUFACTURER.contains("QiKU") || Build.MANUFACTURER.contains("360")
+
+    fun checkIsOppoRom() =
+        Build.MANUFACTURER.contains("OPPO") || Build.MANUFACTURER.contains("oppo")
+
+    fun checkIsVivoRom() =
+        Build.MANUFACTURER.contains("VIVO") || Build.MANUFACTURER.contains("vivo")
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/utils/DefaultDisplayHeight.kt b/easyfloat/src/main/java/com/lzf/easyfloat/utils/DefaultDisplayHeight.kt
new file mode 100644
index 0000000..2479b9a
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/utils/DefaultDisplayHeight.kt
@@ -0,0 +1,15 @@
+package com.lzf.easyfloat.utils
+
+import android.content.Context
+import com.lzf.easyfloat.interfaces.OnDisplayHeight
+
+/**
+ * @author: liuzhenfeng
+ * @function: 获取屏幕有效高度的实现类
+ * @date: 2020-02-16  16:26
+ */
+internal class DefaultDisplayHeight : OnDisplayHeight {
+
+    override fun getDisplayRealHeight(context: Context) = DisplayUtils.rejectedNavHeight(context)
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/utils/DisplayUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/utils/DisplayUtils.kt
new file mode 100644
index 0000000..4e1b091
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/utils/DisplayUtils.kt
@@ -0,0 +1,172 @@
+package com.lzf.easyfloat.utils
+
+import android.app.Service
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Point
+import android.os.Build
+import android.provider.Settings
+import android.util.DisplayMetrics
+import android.view.View
+import android.view.WindowManager
+import com.lzf.easyfloat.permission.rom.RomUtils
+
+/**
+ * @author: liuzhenfeng
+ * @function: 屏幕显示相关工具类
+ * @date: 2019-05-23  15:23
+ */
+object DisplayUtils {
+
+    private const val TAG = "DisplayUtils--->"
+
+    fun px2dp(context: Context, pxVal: Float): Int {
+        val density = context.resources.displayMetrics.density
+        return (pxVal / density + 0.5f).toInt()
+    }
+
+    fun dp2px(context: Context, dpVal: Float): Int {
+        val density = context.resources.displayMetrics.density
+        return (dpVal * density + 0.5f).toInt()
+    }
+
+    fun px2sp(context: Context, pxValue: Float): Int {
+        val fontScale = context.resources.displayMetrics.scaledDensity
+        return (pxValue / fontScale + 0.5f).toInt()
+    }
+
+    fun sp2px(context: Context, spValue: Float): Int {
+        val fontScale = context.resources.displayMetrics.scaledDensity
+        return (spValue * fontScale + 0.5f).toInt()
+    }
+
+    /**
+     * 获取屏幕宽度(显示宽度,横屏的时候可能会小于物理像素值)
+     */
+    fun getScreenWidth(context: Context): Int {
+        val windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
+        val outMetrics = DisplayMetrics()
+        windowManager.defaultDisplay.getMetrics(outMetrics)
+        return outMetrics.widthPixels
+    }
+
+    /**
+     * 获取屏幕高度(物理像素值的高度)
+     */
+    fun getScreenHeight(context: Context) = getScreenSize(context).y
+
+    /**
+     * 获取屏幕宽高
+     */
+    fun getScreenSize(context: Context) = Point().apply {
+        val windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
+        val display = windowManager.defaultDisplay
+        display.getRealSize(this)
+    }
+
+    /**
+     * 获取状态栏高度
+     */
+    fun getStatusBarHeight(context: Context): Int {
+        var result = 0
+        val resources = context.resources
+        val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
+        if (resourceId > 0) result = resources.getDimensionPixelSize(resourceId)
+        return result
+    }
+
+    fun statusBarHeight(view: View) = getStatusBarHeight(view.context.applicationContext)
+
+    /**
+     * 获取导航栏真实的高度(可能未显示)
+     */
+    fun getNavigationBarHeight(context: Context): Int {
+        var result = 0
+        val resources = context.resources
+        val resourceId =
+            resources.getIdentifier("navigation_bar_height", "dimen", "android")
+        if (resourceId > 0) result = resources.getDimensionPixelSize(resourceId)
+        return result
+    }
+
+    /**
+     * 获取导航栏当前的高度
+     */
+    fun getNavigationBarCurrentHeight(context: Context) =
+        if (hasNavigationBar(context)) getNavigationBarHeight(context) else 0
+
+    /**
+     * 判断虚拟导航栏是否显示
+     *
+     * @param context 上下文对象
+     * @return true(显示虚拟导航栏),false(不显示或不支持虚拟导航栏)
+     */
+    fun hasNavigationBar(context: Context) = when {
+        getNavigationBarHeight(context) == 0 -> false
+        RomUtils.checkIsHuaweiRom() && isHuaWeiHideNav(context) -> false
+        RomUtils.checkIsMiuiRom() && isMiuiFullScreen(context) -> false
+        RomUtils.checkIsVivoRom() && isVivoFullScreen(context) -> false
+        else -> isHasNavigationBar(context)
+    }
+
+    /**
+     * 不包含导航栏的有效高度(没有导航栏,或者已去除导航栏的高度)
+     */
+    fun rejectedNavHeight(context: Context): Int {
+        val point = getScreenSize(context)
+        if (point.x > point.y) return point.y
+        return point.y - getNavigationBarCurrentHeight(context)
+    }
+
+    /**
+     * 华为手机是否隐藏了虚拟导航栏
+     * @return true 表示隐藏了,false 表示未隐藏
+     */
+    private fun isHuaWeiHideNav(context: Context) =
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            Settings.System.getInt(context.contentResolver, "navigationbar_is_min", 0)
+        } else {
+            Settings.Global.getInt(context.contentResolver, "navigationbar_is_min", 0)
+        } != 0
+
+    /**
+     * 小米手机是否开启手势操作
+     * @return false 表示使用的是虚拟导航键(NavigationBar), true 表示使用的是手势, 默认是false
+     */
+    private fun isMiuiFullScreen(context: Context) =
+        Settings.Global.getInt(context.contentResolver, "force_fsg_nav_bar", 0) != 0
+
+    /**
+     * Vivo手机是否开启手势操作
+     * @return false 表示使用的是虚拟导航键(NavigationBar), true 表示使用的是手势, 默认是false
+     */
+    private fun isVivoFullScreen(context: Context): Boolean =
+        Settings.Secure.getInt(context.contentResolver, "navigation_gesture_on", 0) != 0
+
+    /**
+     * 其他手机根据屏幕真实高度与显示高度是否相同来判断
+     */
+    private fun isHasNavigationBar(context: Context): Boolean {
+        val windowManager: WindowManager =
+            context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
+        val d = windowManager.defaultDisplay
+
+        val realDisplayMetrics = DisplayMetrics()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            d.getRealMetrics(realDisplayMetrics)
+        }
+        val realHeight = realDisplayMetrics.heightPixels
+        val realWidth = realDisplayMetrics.widthPixels
+
+        val displayMetrics = DisplayMetrics()
+        d.getMetrics(displayMetrics)
+        val displayHeight = displayMetrics.heightPixels
+        val displayWidth = displayMetrics.widthPixels
+
+        // 部分无良厂商的手势操作,显示高度 + 导航栏高度,竟然大于物理高度,对于这种情况,直接默认未启用导航栏
+        if (displayHeight + getNavigationBarHeight(context) > realHeight) return false
+
+        return realWidth - displayWidth > 0 || realHeight - displayHeight > 0
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/utils/DragUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/utils/DragUtils.kt
new file mode 100644
index 0000000..b925724
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/utils/DragUtils.kt
@@ -0,0 +1,177 @@
+package com.lzf.easyfloat.utils
+
+import android.view.*
+import com.lzf.easyfloat.EasyFloat
+import com.lzf.easyfloat.R
+import com.lzf.easyfloat.anim.DefaultAnimator
+import com.lzf.easyfloat.enums.ShowPattern
+import com.lzf.easyfloat.enums.SidePattern
+import com.lzf.easyfloat.interfaces.OnFloatAnimator
+import com.lzf.easyfloat.interfaces.OnTouchRangeListener
+import com.lzf.easyfloat.widget.BaseSwitchView
+
+/**
+ * @author: liuzhenfeng
+ * @date: 2020/10/24  21:29
+ * @Package: com.lzf.easyfloat.utils
+ * @Description: 拖拽打开、关闭浮窗
+ */
+object DragUtils {
+
+    private const val ADD_TAG = "ADD_TAG"
+    private const val CLOSE_TAG = "CLOSE_TAG"
+    private var addView: BaseSwitchView? = null
+    private var closeView: BaseSwitchView? = null
+    private var downX = 0f
+    private var screenWidth = 0
+    private var offset = 0f
+
+    /**
+     * 注册侧滑创建浮窗
+     * @param event Activity 的触摸事件
+     * @param listener 右下角区域触摸事件回调
+     * @param layoutId 右下角区域的布局文件
+     * @param slideOffset 当前屏幕侧滑进度
+     * @param start 动画开始阈值
+     * @param end 动画结束阈值
+     */
+    @JvmOverloads
+    fun registerSwipeAdd(
+        event: MotionEvent?,
+        listener: OnTouchRangeListener? = null,
+        layoutId: Int = R.layout.default_add_layout,
+        slideOffset: Float = -1f,
+        start: Float = 0.1f,
+        end: Float = 0.5f
+    ) {
+        if (event == null) return
+
+        // 设置了侧滑监听,使用侧滑数据
+        if (slideOffset != -1f) {
+            // 如果滑动偏移,超过了动画起始位置,开始显示浮窗,并执行偏移动画
+            if (slideOffset >= start) {
+                val progress = minOf((slideOffset - start) / (end - start), 1f)
+                setAddView(event, progress, listener, layoutId)
+            } else dismissAdd()
+        } else {
+            // 未提供侧滑监听,根据手指坐标信息,判断浮窗信息
+            screenWidth = DisplayUtils.getScreenWidth(LifecycleUtils.application)
+            offset = event.rawX / screenWidth
+            when (event.action) {
+                MotionEvent.ACTION_DOWN -> downX = event.rawX
+                MotionEvent.ACTION_MOVE -> {
+                    // 起始值小于最小边界值,并且当前偏离量大于最小边界
+                    if (downX < start * screenWidth && offset >= start) {
+                        val progress = minOf((offset - start) / (end - start), 1f)
+                        setAddView(event, progress, listener, layoutId)
+                    } else dismissAdd()
+                }
+                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+                    downX = 0f
+                    setAddView(event, offset, listener, layoutId)
+                }
+            }
+        }
+    }
+
+    private fun setAddView(
+        event: MotionEvent,
+        progress: Float,
+        listener: OnTouchRangeListener? = null,
+        layoutId: Int
+    ) {
+        // 设置触摸状态监听
+        addView?.let {
+            it.setTouchRangeListener(event, listener)
+            it.translationX = it.width * (1 - progress)
+            it.translationY = it.width * (1 - progress)
+        }
+        // 手指抬起或者事件取消,关闭添加浮窗
+        if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) dismissAdd()
+        else showAdd(layoutId)
+    }
+
+    private fun showAdd(layoutId: Int) {
+        if (EasyFloat.isShow(ADD_TAG)) return
+        EasyFloat.with(LifecycleUtils.application)
+            .setLayout(layoutId)
+            .setShowPattern(ShowPattern.CURRENT_ACTIVITY)
+            .setTag(ADD_TAG)
+            .setDragEnable(false)
+            .setSidePattern(SidePattern.BOTTOM)
+            .setGravity(Gravity.BOTTOM or Gravity.END)
+            .setAnimator(null)
+            .registerCallback {
+                createResult { isCreated, _, view ->
+                    if (!isCreated || view == null) return@createResult
+                    if ((view as ViewGroup).childCount > 0) {
+                        // 获取区间判断布局
+                        view.getChildAt(0).apply {
+                            if (this is BaseSwitchView) {
+                                addView = this
+                                translationX = width.toFloat()
+                                translationY = width.toFloat()
+                            }
+                        }
+                    }
+                }
+                dismiss { addView = null }
+            }
+            .show()
+    }
+
+    /**
+     * 注册侧滑关闭浮窗
+     * @param event 浮窗的触摸事件
+     * @param listener 关闭区域触摸事件回调
+     * @param layoutId 关闭区域的布局文件
+     * @param showPattern 关闭区域的浮窗类型
+     * @param appFloatAnimator 关闭区域的浮窗出入动画
+     */
+    @JvmOverloads
+    fun registerDragClose(
+        event: MotionEvent,
+        listener: OnTouchRangeListener? = null,
+        layoutId: Int = R.layout.default_close_layout,
+        showPattern: ShowPattern = ShowPattern.CURRENT_ACTIVITY,
+        appFloatAnimator: OnFloatAnimator? = DefaultAnimator()
+    ) {
+        showClose(layoutId, showPattern, appFloatAnimator)
+        // 设置触摸状态监听
+        closeView?.setTouchRangeListener(event, listener)
+        // 抬起手指时,关闭删除选项
+        if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) dismissClose()
+    }
+
+    private fun showClose(
+        layoutId: Int,
+        showPattern: ShowPattern,
+        appFloatAnimator: OnFloatAnimator?
+    ) {
+        if (EasyFloat.isShow(CLOSE_TAG)) return
+        EasyFloat.with(LifecycleUtils.application)
+            .setLayout(layoutId)
+            .setShowPattern(showPattern)
+            .setMatchParent(widthMatch = true)
+            .setTag(CLOSE_TAG)
+            .setSidePattern(SidePattern.BOTTOM)
+            .setGravity(Gravity.BOTTOM)
+            .setAnimator(appFloatAnimator)
+            .registerCallback {
+                createResult { isCreated, _, view ->
+                    if (!isCreated || view == null) return@createResult
+                    if ((view as ViewGroup).childCount > 0) {
+                        // 获取区间判断布局
+                        view.getChildAt(0).apply { if (this is BaseSwitchView) closeView = this }
+                    }
+                }
+                dismiss { closeView = null }
+            }
+            .show()
+    }
+
+    private fun dismissAdd() = EasyFloat.dismiss(ADD_TAG)
+
+    private fun dismissClose() = EasyFloat.dismiss(CLOSE_TAG)
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/utils/InputMethodUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/utils/InputMethodUtils.kt
new file mode 100644
index 0000000..742b972
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/utils/InputMethodUtils.kt
@@ -0,0 +1,60 @@
+package com.lzf.easyfloat.utils
+
+import android.annotation.SuppressLint
+import android.content.Context.INPUT_METHOD_SERVICE
+import android.os.Handler
+import android.os.Looper
+import android.view.MotionEvent
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import com.lzf.easyfloat.core.FloatingWindowManager
+
+/**
+ * @author: liuzhenfeng
+ * @function: 软键盘工具类:解决浮窗内的EditText,无法弹起软键盘的问题
+ * @date: 2019-08-17  11:11
+ */
+object InputMethodUtils {
+
+    @SuppressLint("ClickableViewAccessibility")
+    internal fun initInputMethod(editText: EditText, tag: String? = null) {
+        editText.setOnTouchListener { _, event ->
+            if (event.action == MotionEvent.ACTION_DOWN) openInputMethod(editText, tag)
+            false
+        }
+    }
+
+    /**
+     * 让浮窗获取焦点,并打开软键盘
+     */
+    @JvmStatic
+    @JvmOverloads
+    fun openInputMethod(editText: EditText, tag: String? = null) {
+        FloatingWindowManager.getHelper(tag)?.apply {
+            // 更改flags,并刷新布局,让系统浮窗获取焦点
+            params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+            windowManager.updateViewLayout(frameLayout, params)
+        }
+
+        Handler(Looper.getMainLooper()).postDelayed({
+            // 打开软键盘
+            val inputManager =
+                editText.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager?
+            inputManager?.showSoftInput(editText, 0)
+        }, 100)
+    }
+
+    /**
+     * 当软键盘关闭时,调用此方法,移除系统浮窗的焦点,不然系统返回键无效
+     */
+    @JvmStatic
+    @JvmOverloads
+    fun closedInputMethod(tag: String? = null) =
+        FloatingWindowManager.getHelper(tag)?.run {
+            params.flags =
+                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+            windowManager.updateViewLayout(frameLayout, params)
+        }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/utils/LifecycleUtils.kt b/easyfloat/src/main/java/com/lzf/easyfloat/utils/LifecycleUtils.kt
new file mode 100644
index 0000000..59deeb1
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/utils/LifecycleUtils.kt
@@ -0,0 +1,106 @@
+package com.lzf.easyfloat.utils
+
+import android.app.Activity
+import android.app.Application
+import android.os.Bundle
+import com.lzf.easyfloat.core.FloatingWindowManager
+import com.lzf.easyfloat.enums.ShowPattern
+import java.lang.ref.WeakReference
+
+/**
+ * @author: liuzhenfeng
+ * @function: 通过生命周期回调,判断系统浮窗的过滤信息,以及app是否位于前台,控制浮窗显隐
+ * @date: 2019-07-11  15:51
+ */
+internal object LifecycleUtils {
+
+    lateinit var application: Application
+    private var activityCount = 0
+    private var mTopActivity: WeakReference<Activity>? = null
+
+    fun getTopActivity(): Activity? = mTopActivity?.get()
+
+    fun setLifecycleCallbacks(application: Application) {
+        this.application = application
+        application.registerActivityLifecycleCallbacks(object :
+            Application.ActivityLifecycleCallbacks {
+
+            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
+
+            override fun onActivityStarted(activity: Activity) {
+                // 计算启动的activity数目
+                activity?.let { activityCount++ }
+            }
+
+            override fun onActivityResumed(activity: Activity) {
+                activity?.let {
+                    mTopActivity?.clear()
+                    mTopActivity = WeakReference<Activity>(it)
+                    // 每次都要判断当前页面是否需要显示
+                    checkShow(it)
+                }
+            }
+
+            override fun onActivityPaused(activity: Activity) {}
+
+            override fun onActivityStopped(activity: Activity) {
+                activity?.let {
+                    // 计算关闭的activity数目,并判断当前App是否处于后台
+                    activityCount--
+                    checkHide(it)
+                }
+            }
+
+            override fun onActivityDestroyed(activity: Activity) {}
+
+            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
+        })
+    }
+
+    /**
+     * 判断浮窗是否需要显示
+     */
+    private fun checkShow(activity: Activity) =
+        FloatingWindowManager.windowMap.forEach { (tag, manager) ->
+            manager.config.apply {
+                when {
+                    // 当前页面的浮窗,不需要处理
+                    showPattern == ShowPattern.CURRENT_ACTIVITY -> return@apply
+                    // 仅后台显示模式下,隐藏浮窗
+                    showPattern == ShowPattern.BACKGROUND -> setVisible(false, tag)
+                    // 如果没有手动隐藏浮窗,需要考虑过滤信息
+                    needShow -> setVisible(activity.componentName.className !in filterSet, tag)
+                }
+            }
+        }
+
+    /**
+     * 判断浮窗是否需要隐藏
+     */
+    private fun checkHide(activity: Activity) {
+        // 如果不是finish,并且处于前台,无需判断
+        if (!activity.isFinishing && isForeground()) return
+        FloatingWindowManager.windowMap.forEach { (tag, manager) ->
+            // 判断浮窗是否需要关闭
+            if (activity.isFinishing) manager.params.token?.let {
+                // 如果token不为空,并且是当前销毁的Activity,关闭浮窗,防止窗口泄漏
+                if (it == activity.window?.decorView?.windowToken) {
+                    FloatingWindowManager.dismiss(tag, true)
+                }
+            }
+
+            manager.config.apply {
+                if (!isForeground() && manager.config.showPattern != ShowPattern.CURRENT_ACTIVITY) {
+                    // 当app处于后台时,全局、仅后台显示的浮窗,如果没有手动隐藏,需要显示
+                    setVisible(showPattern != ShowPattern.FOREGROUND && needShow, tag)
+                }
+            }
+        }
+    }
+
+    fun isForeground() = activityCount > 0
+
+    private fun setVisible(isShow: Boolean = isForeground(), tag: String?) =
+        FloatingWindowManager.visible(isShow, tag)
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/utils/Logger.kt b/easyfloat/src/main/java/com/lzf/easyfloat/utils/Logger.kt
new file mode 100644
index 0000000..f2bb9c3
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/utils/Logger.kt
@@ -0,0 +1,48 @@
+package com.lzf.easyfloat.utils
+
+import android.util.Log
+import com.lzf.easyfloat.BuildConfig
+
+/**
+ * @author: liuzhenfeng
+ * @function:
+ * @date: 2019-05-27  16:48
+ */
+internal object Logger {
+
+    private var tag = "EasyFloat--->"
+
+    // 设为false关闭日志
+    private var logEnable = BuildConfig.DEBUG
+
+    fun i(msg: Any) = i(tag, msg.toString())
+
+    fun v(msg: Any) = v(tag, msg.toString())
+
+    fun d(msg: Any) = d(tag, msg.toString())
+
+    fun w(msg: Any) = w(tag, msg.toString())
+
+    fun e(msg: Any) = e(tag, msg.toString())
+
+    fun i(tag: String, msg: String) {
+        if (logEnable) Log.i(tag, msg)
+    }
+
+    fun v(tag: String, msg: String) {
+        if (logEnable) Log.v(tag, msg)
+    }
+
+    fun d(tag: String, msg: String) {
+        if (logEnable) Log.d(tag, msg)
+    }
+
+    fun w(tag: String, msg: String) {
+        if (logEnable) Log.w(tag, msg)
+    }
+
+    fun e(tag: String, msg: String) {
+        if (logEnable) Log.e(tag, msg)
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/widget/BaseSwitchView.kt b/easyfloat/src/main/java/com/lzf/easyfloat/widget/BaseSwitchView.kt
new file mode 100644
index 0000000..5bef66d
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/widget/BaseSwitchView.kt
@@ -0,0 +1,21 @@
+package com.lzf.easyfloat.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.widget.RelativeLayout
+import com.lzf.easyfloat.interfaces.OnTouchRangeListener
+
+/**
+ * @author: liuzhenfeng
+ * @date: 2020/10/25  11:08
+ * @Package: com.lzf.easyfloat.widget
+ * @Description:
+ */
+abstract class BaseSwitchView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : RelativeLayout(context, attrs, defStyleAttr) {
+
+    abstract fun setTouchRangeListener(event: MotionEvent, listener: OnTouchRangeListener? = null)
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultAddView.kt b/easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultAddView.kt
new file mode 100644
index 0000000..88e989a
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultAddView.kt
@@ -0,0 +1,85 @@
+package com.lzf.easyfloat.widget
+
+import android.content.Context
+import android.graphics.*
+import android.util.AttributeSet
+import android.view.MotionEvent
+import com.lzf.easyfloat.interfaces.OnTouchRangeListener
+
+/**
+ * @author: liuzhenfeng
+ * @date: 11/21/20  17:49
+ * @Package: com.lzf.easyfloat.widget
+ * @Description:
+ */
+class DefaultAddView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : BaseSwitchView(context, attrs, defStyleAttr) {
+
+    private lateinit var paint: Paint
+    private var path = Path()
+    private var width = 0f
+    private var height = 0f
+    private var region = Region()
+    private val totalRegion = Region()
+    private var inRange = false
+    private var zoomSize = 18f
+    private var listener: OnTouchRangeListener? = null
+
+    init {
+        initPath()
+        setWillNotDraw(false)
+    }
+
+    private fun initPath() {
+        paint = Paint().apply {
+            color = Color.parseColor("#AA000000")
+            strokeWidth = 10f
+            style = Paint.Style.FILL
+            isAntiAlias = true
+        }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        width = w.toFloat()
+        height = h.toFloat()
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        path.reset()
+        if (inRange) {
+            path.addCircle(width, height, minOf(width, height), Path.Direction.CW)
+        } else {
+            path.addCircle(width, height, minOf(width, height) - zoomSize, Path.Direction.CW)
+            totalRegion.set(zoomSize.toInt(), zoomSize.toInt(), width.toInt(), height.toInt())
+            region.setPath(path, totalRegion)
+        }
+        canvas?.drawPath(path, paint)
+        super.onDraw(canvas)
+    }
+
+    override fun setTouchRangeListener(event: MotionEvent, listener: OnTouchRangeListener?) {
+        this.listener = listener
+        initTouchRange(event)
+    }
+
+    private fun initTouchRange(event: MotionEvent): Boolean {
+        val location = IntArray(2)
+        // 获取在整个屏幕内的绝对坐标
+        getLocationOnScreen(location)
+        val currentInRange = region.contains(
+            event.rawX.toInt() - location[0], event.rawY.toInt() - location[1]
+        )
+        if (currentInRange != inRange) {
+            inRange = currentInRange
+            invalidate()
+        }
+        listener?.touchInRange(currentInRange, this)
+        if (event.action == MotionEvent.ACTION_UP && currentInRange) {
+            listener?.touchUpInRange()
+        }
+        return currentInRange
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultCloseView.kt b/easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultCloseView.kt
new file mode 100644
index 0000000..491d622
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultCloseView.kt
@@ -0,0 +1,154 @@
+package com.lzf.easyfloat.widget
+
+import android.content.Context
+import android.graphics.*
+import android.util.AttributeSet
+import android.view.MotionEvent
+import com.lzf.easyfloat.R
+import com.lzf.easyfloat.interfaces.OnTouchRangeListener
+import com.lzf.easyfloat.utils.DisplayUtils
+
+/**
+ * @author: liuzhenfeng
+ * @date: 2020/10/25  11:16
+ * @Package: com.lzf.easyfloat.widget
+ * @Description:
+ */
+class DefaultCloseView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : BaseSwitchView(context, attrs, defStyleAttr) {
+
+    private var normalColor = Color.parseColor("#99000000")
+    private var inRangeColor = Color.parseColor("#99FF0000")
+    private var shapeType = 0
+
+    private lateinit var paint: Paint
+    private var path = Path()
+    private var width = 0f
+    private var height = 0f
+    private var rectF = RectF()
+    private var region = Region()
+    private val totalRegion = Region()
+    private var inRange = false
+    private var zoomSize = DisplayUtils.dp2px(context, 4f).toFloat()
+    private var listener: OnTouchRangeListener? = null
+
+    init {
+        attrs?.apply { initAttrs(this) }
+        initPaint()
+        setWillNotDraw(false)
+    }
+
+    private fun initAttrs(attrs: AttributeSet) =
+        context.theme.obtainStyledAttributes(attrs, R.styleable.DefaultCloseView, 0, 0).apply {
+            normalColor = getColor(R.styleable.DefaultCloseView_normalColor, normalColor)
+            inRangeColor = getColor(R.styleable.DefaultCloseView_inRangeColor, inRangeColor)
+            shapeType = getInt(R.styleable.DefaultCloseView_shapeType, shapeType)
+            zoomSize = getDimension(R.styleable.DefaultCloseView_zoomSize, zoomSize)
+        }.recycle()
+
+
+    private fun initPaint() {
+        paint = Paint().apply {
+            color = normalColor
+            strokeWidth = 10f
+            style = Paint.Style.FILL
+            isAntiAlias = true
+        }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        width = w.toFloat()
+        height = h.toFloat()
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        path.reset()
+        if (inRange) {
+            paint.color = inRangeColor
+            when (shapeType) {
+                // 半椭圆
+                0 -> {
+                    rectF.set(paddingLeft.toFloat(), 0f, width - paddingRight, height * 2)
+                    path.addOval(rectF, Path.Direction.CW)
+                }
+                // 矩形
+                1 -> {
+                    rectF.set(paddingLeft.toFloat(), 0f, width - paddingRight, height)
+                    path.addRect(rectF, Path.Direction.CW)
+                }
+                // 半圆
+                2 -> path.addCircle(width / 2, height, height, Path.Direction.CW)
+            }
+        } else {
+            paint.color = normalColor
+            when (shapeType) {
+                // 半椭圆
+                0 -> {
+                    rectF.set(
+                        paddingLeft + zoomSize,
+                        zoomSize,
+                        width - paddingRight - zoomSize,
+                        (height - zoomSize) * 2
+                    )
+                    path.addOval(rectF, Path.Direction.CW)
+                    totalRegion.set(
+                        paddingLeft + zoomSize.toInt(),
+                        zoomSize.toInt(),
+                        (width - paddingRight - zoomSize).toInt(),
+                        height.toInt()
+                    )
+                }
+                // 矩形
+                1 -> {
+                    rectF.set(
+                        paddingLeft.toFloat(),
+                        zoomSize,
+                        width - paddingRight,
+                        height
+                    )
+                    path.addRect(rectF, Path.Direction.CW)
+                    totalRegion.set(
+                        paddingLeft,
+                        zoomSize.toInt(),
+                        width.toInt() - paddingRight,
+                        height.toInt()
+                    )
+                }
+                // 半圆
+                2 -> {
+                    path.addCircle(width / 2, height, height - zoomSize, Path.Direction.CW)
+                    totalRegion.set(0, zoomSize.toInt(), width.toInt(), height.toInt())
+                }
+            }
+            region.setPath(path, totalRegion)
+        }
+        canvas?.drawPath(path, paint)
+        super.onDraw(canvas)
+    }
+
+    override fun setTouchRangeListener(event: MotionEvent, listener: OnTouchRangeListener?) {
+        this.listener = listener
+        initTouchRange(event)
+    }
+
+    private fun initTouchRange(event: MotionEvent): Boolean {
+        val location = IntArray(2)
+        // 获取在整个屏幕内的绝对坐标
+        getLocationOnScreen(location)
+        val currentInRange = region.contains(
+            event.rawX.toInt() - location[0], event.rawY.toInt() - location[1]
+        )
+        if (currentInRange != inRange) {
+            inRange = currentInRange
+            invalidate()
+        }
+        listener?.touchInRange(currentInRange, this)
+        if (event.action == MotionEvent.ACTION_UP && currentInRange) {
+            listener?.touchUpInRange()
+        }
+        return currentInRange
+    }
+
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/java/com/lzf/easyfloat/widget/ParentFrameLayout.kt b/easyfloat/src/main/java/com/lzf/easyfloat/widget/ParentFrameLayout.kt
new file mode 100644
index 0000000..2d7d6ff
--- /dev/null
+++ b/easyfloat/src/main/java/com/lzf/easyfloat/widget/ParentFrameLayout.kt
@@ -0,0 +1,72 @@
+package com.lzf.easyfloat.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.widget.FrameLayout
+import com.lzf.easyfloat.data.FloatConfig
+import com.lzf.easyfloat.interfaces.OnFloatTouchListener
+import com.lzf.easyfloat.utils.InputMethodUtils
+
+/**
+ * @author: liuzhenfeng
+ * @function: 系统浮窗的父布局,对touch事件进行了重新分发
+ * @date: 2019-07-10  14:16
+ */
+@SuppressLint("ViewConstructor")
+internal class ParentFrameLayout(
+    context: Context,
+    private val config: FloatConfig,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+    var touchListener: OnFloatTouchListener? = null
+    var layoutListener: OnLayoutListener? = null
+    private var isCreated = false
+
+    // 布局绘制完成的接口,用于通知外部做一些View操作,不然无法获取view宽高
+    interface OnLayoutListener {
+        fun onLayout()
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        // 初次绘制完成的时候,需要设置对齐方式、坐标偏移量、入场动画
+        if (!isCreated) {
+            isCreated = true
+            layoutListener?.onLayout()
+        }
+    }
+
+    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
+        if (event != null) touchListener?.onTouch(event)
+        // 是拖拽事件就进行拦截,反之不拦截
+        // ps:拦截后将不再回调该方法,会交给该view的onTouchEvent进行处理,所以后续事件需要在onTouchEvent中回调
+        return config.isDrag || super.onInterceptTouchEvent(event)
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        if (event != null) touchListener?.onTouch(event)
+        return config.isDrag || super.onTouchEvent(event)
+    }
+
+    /**
+     * 按键转发到视图的分发方法,在这里关闭输入法
+     */
+    override fun dispatchKeyEventPreIme(event: KeyEvent?): Boolean {
+        if (config.hasEditText && event?.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_BACK) {
+            InputMethodUtils.closedInputMethod(config.floatTag)
+        }
+        return super.dispatchKeyEventPreIme(event)
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        config.callbacks?.dismiss()
+        config.floatCallbacks?.builder?.dismiss?.invoke()
+    }
+}
\ No newline at end of file
diff --git a/easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_normal.png b/easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_normal.png
new file mode 100644
index 0000000..161278d
--- /dev/null
+++ b/easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_normal.png
Binary files differ
diff --git a/easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_selected.png b/easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_selected.png
new file mode 100644
index 0000000..6037ac4
--- /dev/null
+++ b/easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_selected.png
Binary files differ
diff --git a/easyfloat/src/main/res/drawable/add_normal.xml b/easyfloat/src/main/res/drawable/add_normal.xml
new file mode 100644
index 0000000..69c60a6
--- /dev/null
+++ b/easyfloat/src/main/res/drawable/add_normal.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+                android:innerRadius="9dp"
+                android:shape="ring"
+                android:thickness="1dp"
+                android:useLevel="false">
+            <stroke
+                    android:width="1dp"
+                    android:color="#ffffff" />
+        </shape>
+    </item>
+    <item>
+        <shape
+                android:innerRadius="16dp"
+                android:shape="ring"
+                android:thickness="1dp"
+                android:useLevel="false">
+            <stroke
+                    android:width="1dp"
+                    android:color="#ffffff" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/drawable/add_selected.xml b/easyfloat/src/main/res/drawable/add_selected.xml
new file mode 100644
index 0000000..a7caecc
--- /dev/null
+++ b/easyfloat/src/main/res/drawable/add_selected.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+                android:innerRadius="9dp"
+                android:shape="ring"
+                android:thickness="1dp"
+                android:useLevel="false">
+            <stroke
+                    android:width="1dp"
+                    android:color="#ffffff" />
+        </shape>
+    </item>
+    <item>
+        <shape
+                android:innerRadius="20dp"
+                android:shape="ring"
+                android:thickness="1dp"
+                android:useLevel="false">
+            <stroke
+                    android:width="1dp"
+                    android:color="#ffffff" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/drawable/base_rauis_for_white.xml b/easyfloat/src/main/res/drawable/base_rauis_for_white.xml
new file mode 100644
index 0000000..708156f
--- /dev/null
+++ b/easyfloat/src/main/res/drawable/base_rauis_for_white.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <corners android:radius="9dp"/>
+    <solid android:color="#ffffff"/>
+</shape>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/drawable/bg_yellow_22dp.xml b/easyfloat/src/main/res/drawable/bg_yellow_22dp.xml
new file mode 100644
index 0000000..f59ad92
--- /dev/null
+++ b/easyfloat/src/main/res/drawable/bg_yellow_22dp.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="22dp"/>
+    <solid android:color="#FFDC04"/>
+</shape>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/drawable/icon_delete_normal.png b/easyfloat/src/main/res/drawable/icon_delete_normal.png
new file mode 100644
index 0000000..2aa9c7f
--- /dev/null
+++ b/easyfloat/src/main/res/drawable/icon_delete_normal.png
Binary files differ
diff --git a/easyfloat/src/main/res/drawable/icon_delete_selected.png b/easyfloat/src/main/res/drawable/icon_delete_selected.png
new file mode 100644
index 0000000..34a200c
--- /dev/null
+++ b/easyfloat/src/main/res/drawable/icon_delete_selected.png
Binary files differ
diff --git a/easyfloat/src/main/res/layout/default_add_layout.xml b/easyfloat/src/main/res/layout/default_add_layout.xml
new file mode 100644
index 0000000..47a03d7
--- /dev/null
+++ b/easyfloat/src/main/res/layout/default_add_layout.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.lzf.easyfloat.widget.DefaultAddView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="160dp"
+        android:layout_height="160dp"
+        android:clipChildren="false">
+
+    <TextView
+            android:id="@+id/tv_add"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="24dp"
+            android:gravity="center"
+            android:paddingStart="26dp"
+            android:text="@string/add_floating_window"
+            android:textColor="#FFFFFF"
+            android:textSize="12sp" />
+
+    <ImageView
+            android:id="@+id/iv_add"
+            android:layout_width="70dp"
+            android:layout_height="44dp"
+            android:layout_above="@id/tv_add"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="6dp"
+            android:paddingStart="26dp"
+            android:src="@drawable/add_normal" />
+
+</com.lzf.easyfloat.widget.DefaultAddView>
diff --git a/easyfloat/src/main/res/layout/default_close_layout.xml b/easyfloat/src/main/res/layout/default_close_layout.xml
new file mode 100644
index 0000000..14dd80b
--- /dev/null
+++ b/easyfloat/src/main/res/layout/default_close_layout.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.lzf.easyfloat.widget.DefaultCloseView 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="110dp"
+        android:paddingHorizontal="50dp"
+        app:inRangeColor="#99FF0000"
+        app:normalColor="#99000000"
+        app:shapeType="oval"
+        app:zoomSize="4dp">
+
+    <TextView
+            android:id="@+id/tv_delete"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="24dp"
+            android:gravity="center"
+            android:text="@string/delete_floating_window"
+            android:textColor="#FFFFFF"
+            android:textSize="12sp" />
+
+    <ImageView
+            android:id="@+id/iv_delete"
+            android:layout_width="20dp"
+            android:layout_height="20dp"
+            android:layout_above="@id/tv_delete"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="6dp"
+            android:src="@drawable/icon_delete_normal" />
+
+</com.lzf.easyfloat.widget.DefaultCloseView>
diff --git a/easyfloat/src/main/res/layout/window_dialog_common.xml b/easyfloat/src/main/res/layout/window_dialog_common.xml
new file mode 100644
index 0000000..857eae5
--- /dev/null
+++ b/easyfloat/src/main/res/layout/window_dialog_common.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:background="@color/transparent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="35dp"
+        android:layout_marginRight="35dp"
+        android:background="@drawable/base_rauis_for_white"
+        android:gravity="center"
+        android:orientation="vertical"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <LinearLayout
+            android:id="@+id/ll_common"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/img_close"
+                android:layout_width="27dp"
+                android:layout_height="27dp"
+                android:layout_gravity="right"
+                android:layout_marginRight="13dp"
+                android:layout_marginTop="18dp"
+                android:src="@mipmap/ic_dialog_close" />
+
+            <ImageView
+                android:id="@+id/img_title_view"
+                android:layout_gravity="center"
+                android:layout_marginTop="-15dp"
+                android:layout_width="60dp"
+                android:layout_height="60dp"
+                android:src="@mipmap/icon_fuchuang"
+                android:layout_marginBottom="15dp"
+                />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="25dp"
+                android:layout_marginRight="25dp"
+                android:orientation="vertical">
+
+                <TextView
+                    android:id="@+id/text_title"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:singleLine="true"
+                    android:text="浮窗权限未获取"
+                    android:textStyle="bold"
+                    android:textColor="#000000"
+                    android:textSize="19sp" />
+
+
+                <TextView
+                    android:id="@+id/text_dec"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:layout_marginTop="10dp"
+                    android:text="你的手机没有授权微信获得浮窗权限,音乐浮窗不能正常使用"
+                    android:textSize="15sp" />
+
+
+            </LinearLayout>
+        </LinearLayout>
+
+
+        <LinearLayout
+            android:id="@+id/ll_vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="30dp"
+            android:layout_marginBottom="30dp"
+            android:layout_marginLeft="35dp"
+            android:layout_marginRight="35dp"
+            android:gravity="center"
+            android:visibility="visible"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/btn_vertical_sure"
+                android:layout_width="wrap_content"
+                android:layout_height="44dp"
+                android:minWidth="110dp"
+                android:paddingLeft="37dp"
+                android:paddingRight="37dp"
+                android:gravity="center"
+                android:background="@drawable/bg_yellow_22dp"
+                android:text="确定"
+                android:textColor="#000000" />
+
+
+
+        </LinearLayout>
+
+
+    </LinearLayout>
+
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/mipmap-xxhdpi/ic_dialog_close.png b/easyfloat/src/main/res/mipmap-xxhdpi/ic_dialog_close.png
new file mode 100644
index 0000000..6d27ab0
--- /dev/null
+++ b/easyfloat/src/main/res/mipmap-xxhdpi/ic_dialog_close.png
Binary files differ
diff --git a/easyfloat/src/main/res/mipmap-xxhdpi/icon_fuchuang.png b/easyfloat/src/main/res/mipmap-xxhdpi/icon_fuchuang.png
new file mode 100644
index 0000000..58a3299
--- /dev/null
+++ b/easyfloat/src/main/res/mipmap-xxhdpi/icon_fuchuang.png
Binary files differ
diff --git a/easyfloat/src/main/res/values/attrs.xml b/easyfloat/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..76a4f41
--- /dev/null
+++ b/easyfloat/src/main/res/values/attrs.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+
+    <!--圆弧进度条-->
+    <declare-styleable name="TasksCompletedView">
+        <!--内圆半径-->
+        <attr name="radius" format="dimension" />
+        <!--内圆颜色-->
+        <attr name="circleColor" format="color" />
+        <!--进度条宽度-->
+        <attr name="progressWidth" format="dimension" />
+        <!--进度条颜色-->
+        <attr name="progressColor" format="color" />
+        <!--进度条背景色-->
+        <attr name="progressBgColor" format="color" />
+        <!--进度条中间的文字-->
+        <attr name="progressText" format="string" />
+        <!--进度条中间的文字大小-->
+        <attr name="progressTextSize" format="dimension" />
+        <!--进度条中间的文字颜色-->
+        <attr name="progressTextColor" format="color" />
+    </declare-styleable>
+
+    <declare-styleable name="CircleLoadingView">
+        <!--圆弧宽度-->
+        <attr name="arcWidth" format="dimension" />
+        <!--加载动画的颜色-->
+        <attr name="loadingColor" format="color" />
+        <!--环形圆点的数量-->
+        <attr name="dotSize" format="integer" />
+        <!--圆环转动一周的时间-->
+        <attr name="durationTime" format="float" />
+        <!--每周期圆点旋转的角度-->
+        <attr name="dotAngle" format="float" />
+    </declare-styleable>
+
+    <declare-styleable name="DefaultCloseView">
+        <!--区域默认颜色-->
+        <attr name="normalColor" format="color" />
+        <!--区域选中颜色-->
+        <attr name="inRangeColor" format="color" />
+        <!--区域形状-->
+        <attr name="shapeType" format="enum">
+            <enum name="oval" value="0" />
+            <enum name="rect" value="1" />
+            <enum name="circle" value="2" />
+        </attr>
+        <!--选中切换时的缩放大小-->
+        <attr name="zoomSize" format="dimension" />
+    </declare-styleable>
+
+</resources>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/values/color.xml b/easyfloat/src/main/res/values/color.xml
new file mode 100644
index 0000000..216e4a4
--- /dev/null
+++ b/easyfloat/src/main/res/values/color.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="transparent">@android:color/transparent</color>
+</resources>
\ No newline at end of file
diff --git a/easyfloat/src/main/res/values/strings.xml b/easyfloat/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6f56364
--- /dev/null
+++ b/easyfloat/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">EasyFloat</string>
+    <string name="add_floating_window">浮窗</string>
+    <string name="delete_floating_window">删除浮窗</string>
+</resources>
diff --git a/easyfloat/src/main/res/values/styles.xml b/easyfloat/src/main/res/values/styles.xml
new file mode 100644
index 0000000..3bc6a5e
--- /dev/null
+++ b/easyfloat/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="WindowBottomDialog" parent="android:Theme.Dialog">
+        <item name="android:windowIsTranslucent">false</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:backgroundDimEnabled">true</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:baselineAlignBottom">true</item>
+        <item name="android:windowFrame">@null</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/easyfloat/src/test/java/com/lzf/easyfloat/ExampleUnitTest.java b/easyfloat/src/test/java/com/lzf/easyfloat/ExampleUnitTest.java
new file mode 100644
index 0000000..58473e8
--- /dev/null
+++ b/easyfloat/src/test/java/com/lzf/easyfloat/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.lzf.easyfloat;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 37f10d1..48dbf99 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
+include ':easyfloat'
 include ':imagepicker'
 include ':dkplayer-players:exo'
 include ':dkplayer-java'

--
Gitblit v1.7.1