fix
lmw
2025-03-04 449bdb5d2b5bf7b272ca5cda4c066f9a65040064
fix
64个文件已添加
11个文件已修改
4252 ■■■■■ 已修改文件
.idea/gradle.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.idea/jarRepositories.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/sinata/xqmuse/MainActivity.kt 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/sinata/xqmuse/ThinkAudioService.kt 126 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/sinata/xqmuse/network/Apis.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/sinata/xqmuse/ui/home/HomeFragment.kt 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/sinata/xqmuse/ui/home/VoiceDetailActivity.kt 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/sinata/xqmuse/utils/Const.kt 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/layout_floter.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/build.gradle 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/proguard-rules.pro 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/androidTest/java/com/lzf/easyfloat/ExampleInstrumentedTest.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/AndroidManifest.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/BaseEventWindow.kt 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/EasyFloat.kt 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatInitializer.kt 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatMessage.kt 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/WindowDIalog.kt 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/WindowEvent.kt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/anim/AnimatorManager.kt 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/anim/DefaultAnimator.kt 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowHelper.kt 337 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowManager.kt 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/core/TouchUtils.kt 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/data/FloatConfig.kt 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/enums/ShowPattern.kt 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/enums/SidePattern.kt 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/FloatCallbacks.kt 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnDisplayHeight.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatAnimator.kt 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatCallbacks.kt 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatTouchListener.kt 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnInvokeView.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnPermissionResult.kt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnTouchRangeListener.kt 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionFragment.kt 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionUtils.kt 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/HuaweiUtils.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MeizuUtils.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MiuiUtils.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/OppoUtils.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/QikuUtils.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/RomUtils.kt 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/utils/DefaultDisplayHeight.kt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/utils/DisplayUtils.kt 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/utils/DragUtils.kt 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/utils/InputMethodUtils.kt 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/utils/LifecycleUtils.kt 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/utils/Logger.kt 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/widget/BaseSwitchView.kt 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultAddView.kt 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultCloseView.kt 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/java/com/lzf/easyfloat/widget/ParentFrameLayout.kt 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_normal.png 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_selected.png 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable/add_normal.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable/add_selected.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable/base_rauis_for_white.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable/bg_yellow_22dp.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable/icon_delete_normal.png 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/drawable/icon_delete_selected.png 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/layout/default_add_layout.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/layout/default_close_layout.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/layout/window_dialog_common.xml 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/mipmap-xxhdpi/ic_dialog_close.png 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/mipmap-xxhdpi/icon_fuchuang.png 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/values/attrs.xml 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/values/color.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/values/strings.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/main/res/values/styles.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easyfloat/src/test/java/com/lzf/easyfloat/ExampleUnitTest.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
settings.gradle 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.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" />
.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>
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')
}
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>
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 //自动结束的时间戳
    }
}
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 //自动结束的时间戳
    }
}
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"
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)
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()
        }
    }
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 //获取新的播放进度
    }
}
app/src/main/res/layout/layout_floter.xml
New file
@@ -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>
easyfloat/.gitignore
New file
@@ -0,0 +1 @@
/build
easyfloat/build.gradle
New file
@@ -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()
}
easyfloat/proguard-rules.pro
New file
@@ -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
easyfloat/src/androidTest/java/com/lzf/easyfloat/ExampleInstrumentedTest.java
New file
@@ -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());
    }
}
easyfloat/src/main/AndroidManifest.xml
New file
@@ -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>
easyfloat/src/main/java/com/lzf/easyfloat/BaseEventWindow.kt
New file
@@ -0,0 +1,4 @@
package com.lzf.easyfloat
open class BaseEventWindow {
}
easyfloat/src/main/java/com/lzf/easyfloat/EasyFloat.kt
New file
@@ -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)
            }
        }
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatInitializer.kt
New file
@@ -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
}
easyfloat/src/main/java/com/lzf/easyfloat/EasyFloatMessage.kt
New file
@@ -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."
easyfloat/src/main/java/com/lzf/easyfloat/WindowDIalog.kt
New file
@@ -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?)
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/WindowEvent.kt
New file
@@ -0,0 +1,3 @@
package com.lzf.easyfloat
data class WindowEvent(var type : Int) : BaseEventWindow()
easyfloat/src/main/java/com/lzf/easyfloat/anim/AnimatorManager.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/anim/DefaultAnimator.kt
New file
@@ -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
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowHelper.kt
New file
@@ -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)
            }
        }
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/core/FloatingWindowManager.kt
New file
@@ -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)]
}
easyfloat/src/main/java/com/lzf/easyfloat/core/TouchUtils.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/data/FloatConfig.kt
New file
@@ -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
)
easyfloat/src/main/java/com/lzf/easyfloat/enums/ShowPattern.kt
New file
@@ -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
}
easyfloat/src/main/java/com/lzf/easyfloat/enums/SidePattern.kt
New file
@@ -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
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/FloatCallbacks.kt
New file
@@ -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
        }
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnDisplayHeight.java
New file
@@ -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);
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatAnimator.kt
New file
@@ -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
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatCallbacks.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnFloatTouchListener.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnInvokeView.java
New file
@@ -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);
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnPermissionResult.kt
New file
@@ -0,0 +1,11 @@
package com.lzf.easyfloat.interfaces
/**
 * @author: liuzhenfeng
 * @function: 浮窗权限的申请结果
 * @date: 2019-07-15  16:18
 */
interface OnPermissionResult {
    fun permissionResult(isOpen: Boolean)
}
easyfloat/src/main/java/com/lzf/easyfloat/interfaces/OnTouchRangeListener.kt
New file
@@ -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()
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionFragment.kt
New file
@@ -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)
        }
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/PermissionUtils.kt
New file
@@ -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")
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/HuaweiUtils.java
New file
@@ -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;
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MeizuUtils.java
New file
@@ -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;
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/MiuiUtils.java
New file
@@ -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!");
            }
        }
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/OppoUtils.java
New file
@@ -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();
        }
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/QikuUtils.java
New file
@@ -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;
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/permission/rom/RomUtils.kt
New file
@@ -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")
}
easyfloat/src/main/java/com/lzf/easyfloat/utils/DefaultDisplayHeight.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/utils/DisplayUtils.kt
New file
@@ -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
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/utils/DragUtils.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/utils/InputMethodUtils.kt
New file
@@ -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)
        }
}
easyfloat/src/main/java/com/lzf/easyfloat/utils/LifecycleUtils.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/utils/Logger.kt
New file
@@ -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)
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/widget/BaseSwitchView.kt
New file
@@ -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)
}
easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultAddView.kt
New file
@@ -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
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/widget/DefaultCloseView.kt
New file
@@ -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
    }
}
easyfloat/src/main/java/com/lzf/easyfloat/widget/ParentFrameLayout.kt
New file
@@ -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()
    }
}
easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_normal.png
easyfloat/src/main/res/drawable-xxxhdpi/icon_delete_selected.png
easyfloat/src/main/res/drawable/add_normal.xml
New file
@@ -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>
easyfloat/src/main/res/drawable/add_selected.xml
New file
@@ -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>
easyfloat/src/main/res/drawable/base_rauis_for_white.xml
New file
@@ -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>
easyfloat/src/main/res/drawable/bg_yellow_22dp.xml
New file
@@ -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>
easyfloat/src/main/res/drawable/icon_delete_normal.png
easyfloat/src/main/res/drawable/icon_delete_selected.png
easyfloat/src/main/res/layout/default_add_layout.xml
New file
@@ -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>
easyfloat/src/main/res/layout/default_close_layout.xml
New file
@@ -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>
easyfloat/src/main/res/layout/window_dialog_common.xml
New file
@@ -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>
easyfloat/src/main/res/mipmap-xxhdpi/ic_dialog_close.png
easyfloat/src/main/res/mipmap-xxhdpi/icon_fuchuang.png
easyfloat/src/main/res/values/attrs.xml
New file
@@ -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>
easyfloat/src/main/res/values/color.xml
New file
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="transparent">@android:color/transparent</color>
</resources>
easyfloat/src/main/res/values/strings.xml
New file
@@ -0,0 +1,5 @@
<resources>
    <string name="app_name">EasyFloat</string>
    <string name="add_floating_window">浮窗</string>
    <string name="delete_floating_window">删除浮窗</string>
</resources>
easyfloat/src/main/res/values/styles.xml
New file
@@ -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>
easyfloat/src/test/java/com/lzf/easyfloat/ExampleUnitTest.java
New file
@@ -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);
    }
}
settings.gradle
@@ -1,3 +1,4 @@
include ':easyfloat'
include ':imagepicker'
include ':dkplayer-players:exo'
include ':dkplayer-java'