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