| | |
| | | <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" /> |
| | |
| | | <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> |
| | |
| | | |
| | | defaultConfig { |
| | | applicationId "com.sinata.xqmuse" |
| | | minSdkVersion 21 |
| | | minSdkVersion 23 |
| | | targetSdkVersion 30 |
| | | versionCode 10 |
| | | versionName "1.81" |
| | |
| | | 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') |
| | | } |
| | |
| | | <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" /> |
| | |
| | | <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> |
| | |
| | | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | |
| | | 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 |
| | |
| | | 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 { |
| | |
| | | } |
| | | |
| | | 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() |
| | |
| | | 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() |
| | |
| | | */ |
| | | 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() //记录开始冥想的时间 |
| | |
| | | */ |
| | | 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() //对比当前音频是否是每日疗愈 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 申请浮窗权限 增加稳定性 |
| | | */ |
| | | 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 秒") |
| | |
| | | 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 //正在播放疗愈,无法立即切换背景音乐 |
| | |
| | | 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() |
| | |
| | | @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() |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | 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) |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | 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 //自动结束的时间戳 |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | private val focusChangeListener = |
| | | AudioManager.OnAudioFocusChangeListener { focusChange -> |
| | | if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { |
| | | mediaPlayer!!.pause() // 焦点丢失时暂停播放 |
| | | thinkBgPlayer?.pause() // 焦点丢失时暂停播放 |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | // 启动前台服务 |
| | | 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 //自动结束的时间戳 |
| | | } |
| | | } |
| | |
| | | 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" |
| | |
| | | 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 |
| | |
| | | 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) |
| | |
| | | }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)) |
| | | } |
| | | }){_,_-> |
| | |
| | | } |
| | | |
| | | 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) |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | 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) |
| | | } |
| | |
| | | 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() |
| | | } |
| | |
| | | } |
| | | |
| | | 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() |
| | |
| | | } |
| | | |
| | | 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 |
| | | } |
| | |
| | | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | 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() |
| | | } |
| | | } |
| | | |
| | |
| | | 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 //获取新的播放进度 |
| | | |
| | | |
| | | } |
| | | } |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | 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() |
| | | } |
New file |
| | |
| | | # 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 |
New file |
| | |
| | | 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()); |
| | | } |
| | | } |
New file |
| | |
| | | <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> |
New file |
| | |
| | | package com.lzf.easyfloat |
| | | |
| | | open class BaseEventWindow { |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
New file |
| | |
| | | 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." |
New file |
| | |
| | | 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?) |
| | | } |
| | | } |
New file |
| | |
| | | package com.lzf.easyfloat |
| | | |
| | | data class WindowEvent(var type : Int) : BaseEventWindow() |
New file |
| | |
| | | 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) |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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)] |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | |
| | | } |
New file |
| | |
| | | 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 |
| | | |
| | | ) |
New file |
| | |
| | | package com.lzf.easyfloat.enums |
| | | |
| | | /** |
| | | * @author: liuzhenfeng |
| | | * @function: 浮窗显示类别 |
| | | * @date: 2019-07-08 17:05 |
| | | */ |
| | | enum class ShowPattern { |
| | | |
| | | // 只在当前Activity显示、仅应用前台时显示、仅应用后台时显示,一直显示(不分前后台) |
| | | CURRENT_ACTIVITY, FOREGROUND, BACKGROUND, ALL_TIME |
| | | } |
New file |
| | |
| | | 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 |
| | | |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
New file |
| | |
| | | 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 |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
New file |
| | |
| | | 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); |
| | | } |
New file |
| | |
| | | package com.lzf.easyfloat.interfaces |
| | | |
| | | /** |
| | | * @author: liuzhenfeng |
| | | * @function: 浮窗权限的申请结果 |
| | | * @date: 2019-07-15 16:18 |
| | | */ |
| | | interface OnPermissionResult { |
| | | |
| | | fun permissionResult(isOpen: Boolean) |
| | | } |
New file |
| | |
| | | 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() |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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") |
| | | } |
| | | |
| | | } |
| | | |
New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
| | | |
| | | |
New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
New file |
| | |
| | | /* |
| | | * 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!"); |
| | | } |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
New file |
| | |
| | | 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") |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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) |
| | | |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | 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() |
| | | } |
| | | } |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | <?xml version="1.0" encoding="utf-8"?> |
| | | <resources> |
| | | <color name="transparent">@android:color/transparent</color> |
| | | </resources> |
New file |
| | |
| | | <resources> |
| | | <string name="app_name">EasyFloat</string> |
| | | <string name="add_floating_window">浮窗</string> |
| | | <string name="delete_floating_window">删除浮窗</string> |
| | | </resources> |
New file |
| | |
| | | <?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> |
New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | include ':easyfloat' |
| | | include ':imagepicker' |
| | | include ':dkplayer-players:exo' |
| | | include ':dkplayer-java' |