[3.3.0][M1] 360环视需求代码提交
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
package com.mogo.eagle.core.utilcode.floating
|
||||
|
||||
import android.annotation.*
|
||||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.PopupWindow
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.view.doOnAttach
|
||||
import com.mogo.eagle.core.utilcode.kotlin.lifeCycleScope
|
||||
import com.mogo.eagle.core.utilcode.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MoGoPopWindow private constructor(builder: Builder){
|
||||
|
||||
|
||||
private val content: View = builder.content ?: throw AssertionError("要填充的View不能为空")
|
||||
|
||||
private val pop: PopupWindow by lazy { PopupWindow(builder.width, builder.height) }
|
||||
|
||||
private val offsetX: Int = builder.offsetX
|
||||
|
||||
private var offsetY: Int = builder.offsetY
|
||||
|
||||
private var gravityInActivity: Int = builder.gravityInActivity
|
||||
|
||||
private val activity: Activity = builder.attachToActivity ?: throw AssertionError("要依附Activity实例不能为空")
|
||||
|
||||
private val isDraggable: Boolean = builder.isDraggable
|
||||
|
||||
private val isDismissOnTouchOutside: Boolean = builder.isDismissOnTouchOutside
|
||||
|
||||
private val isPendingShow by lazy { AtomicBoolean(false) }
|
||||
|
||||
private val onDismissed: (() -> Unit)? = builder.onDismissed
|
||||
|
||||
private val onShowed: (() -> Unit)? = builder.onShowed
|
||||
|
||||
private val onTouchInterceptorInDrag: ((child: View, event: MotionEvent) -> Boolean)? = builder.onTouchInterceptorInDrag
|
||||
|
||||
private val onViewAttachedToWindow: ((content: View) -> Unit)? = builder.onViewAttachedToWindow
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility") fun show() {
|
||||
if (isPendingShow.get() || pop.isShowing) {
|
||||
return
|
||||
}
|
||||
isPendingShow.set(true)
|
||||
activity.lifeCycleScope.launchWhenResumed {
|
||||
val parent = activity.findViewById<View>(android.R.id.content) ?: throw AssertionError("附着的Activity上的ID为[android.R.id.content]的控件不存在.")
|
||||
pop.contentView = content
|
||||
pop.isAttachedInDecor = true
|
||||
pop.setBackgroundDrawable(ColorDrawable())
|
||||
content.doOnAttach {
|
||||
onViewAttachedToWindow?.invoke(content)
|
||||
onShowed?.invoke()
|
||||
}
|
||||
pop.setOnDismissListener {
|
||||
onDismissed?.invoke()
|
||||
}
|
||||
pop.isTouchable = true
|
||||
if (isDismissOnTouchOutside) {
|
||||
pop.isFocusable = true
|
||||
pop.isOutsideTouchable = true
|
||||
} else {
|
||||
pop.isFocusable= false
|
||||
pop.isOutsideTouchable = false
|
||||
}
|
||||
if (isDraggable) {
|
||||
val outer = Rect()
|
||||
var oldRawX = 0f
|
||||
var oldRawY = 0f
|
||||
var isDownConsumed = false
|
||||
pop.setTouchInterceptor { _, event ->
|
||||
if (event == null) {
|
||||
return@setTouchInterceptor false
|
||||
}
|
||||
val rawX = event.rawX
|
||||
val rawY = event.rawY
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
val child = findChildInContent(content.parent as? ViewGroup, event.x, event.y) ?: content
|
||||
Log.d(TAG, "child -> : ${child.javaClass.simpleName}")
|
||||
if (onTouchInterceptorInDrag?.invoke(child, event) == true) {
|
||||
isDownConsumed = true
|
||||
return@setTouchInterceptor false
|
||||
}
|
||||
if (outer.isEmpty) {
|
||||
val location = IntArray(2)
|
||||
parent.getLocationOnScreen(location)
|
||||
val left = location[0]
|
||||
val top = location[1]
|
||||
val right = left + parent.width
|
||||
val bottom = top + parent.height
|
||||
outer.set(left,top, right, bottom)
|
||||
}
|
||||
oldRawX = rawX
|
||||
oldRawY = rawY
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_MOVE) {
|
||||
if (!isDownConsumed) {
|
||||
val dx = (rawX - oldRawX).toInt()
|
||||
val dy = (rawY - oldRawY).toInt()
|
||||
val params = content.rootView.layoutParams as WindowManager.LayoutParams
|
||||
val oldX = params.x
|
||||
val oldY = params.y
|
||||
val newX = oldX + dx
|
||||
val newY = oldY + dy
|
||||
if (outer.contains(rawX.toInt(), rawY.toInt())) {
|
||||
pop.update(newX, newY, content.width, content.height)
|
||||
}
|
||||
oldRawX = rawX
|
||||
oldRawY = rawY
|
||||
}
|
||||
}
|
||||
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
if (isDownConsumed) {
|
||||
isDownConsumed = false
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
pop.showAtLocation(parent, gravityInActivity, offsetX, offsetY)
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun findChildInContent(group: ViewGroup?, x: Float, y: Float): View? {
|
||||
if (group == null) {
|
||||
return null
|
||||
}
|
||||
val count = group.childCount
|
||||
if (count == 0) {
|
||||
return null
|
||||
}
|
||||
for (i in (count - 1) downTo 0) {
|
||||
val child = group.getChildAt(i)
|
||||
val location = IntArray(2)
|
||||
child.getLocationInWindow(location)
|
||||
val rect = Rect(location[0], location[1], location[0] + child.width, location[1] + child.height)
|
||||
val intX = x.toInt()
|
||||
val intY = y.toInt()
|
||||
if (child is ViewGroup) {
|
||||
if (rect.contains(intX, intY)) {
|
||||
return findChildInContent(child, x, y)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (rect.contains(intX, intY)) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
if (!pop.isShowing) {
|
||||
return
|
||||
}
|
||||
activity.lifeCycleScope.launchWhenResumed {
|
||||
pop.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun isShowing(): Boolean = isPendingShow.get() || pop.isShowing
|
||||
|
||||
class Builder {
|
||||
|
||||
/**
|
||||
* 内容控件实例,设置了此字段,不能再设置[layoutId]
|
||||
*/
|
||||
internal var content: View? = null
|
||||
|
||||
/**
|
||||
* 内容控件的布局ID,设置了此字段,不能再设置[content]
|
||||
*/
|
||||
internal var layoutId: Int? = null
|
||||
|
||||
/**
|
||||
* 当前窗口是否可拖拽
|
||||
*/
|
||||
internal var isDraggable: Boolean = false
|
||||
|
||||
/**
|
||||
* 点击弹窗外部区域,是否消失
|
||||
*/
|
||||
internal var isDismissOnTouchOutside: Boolean = false
|
||||
|
||||
/**
|
||||
* 依附的Activity实例
|
||||
*/
|
||||
internal var attachToActivity: Activity? = null
|
||||
|
||||
/**
|
||||
* 相对于父控件X方向上的偏移量
|
||||
*/
|
||||
internal var offsetX: Int = 0
|
||||
|
||||
/**
|
||||
* 相对于父控件Y方向上的偏移量
|
||||
*/
|
||||
internal var offsetY: Int = 0
|
||||
|
||||
/**
|
||||
* 相对于父控件的布局位置
|
||||
*/
|
||||
internal var gravityInActivity: Int = Gravity.NO_GRAVITY
|
||||
|
||||
internal var width: Int = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
|
||||
internal var height: Int = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
|
||||
internal var onShowed:(()-> Unit)? = null
|
||||
|
||||
internal var onDismissed: (()-> Unit)? = null
|
||||
|
||||
/**
|
||||
* 当处于拖拽状态时,是响应拖拽还是当前窗口自己的事件
|
||||
*/
|
||||
internal var onTouchInterceptorInDrag: ((childInContent: View, event: MotionEvent) -> Boolean)? = null
|
||||
|
||||
|
||||
/**
|
||||
* 当控件被attach到窗口的时候,回调
|
||||
*/
|
||||
internal var onViewAttachedToWindow: ((content: View) -> Unit)? = null
|
||||
|
||||
fun contentView(content: View) = apply {
|
||||
this.content = content
|
||||
}
|
||||
|
||||
fun layoutId(@LayoutRes id: Int) = apply {
|
||||
this.layoutId = id
|
||||
}
|
||||
|
||||
fun draggable(draggable: Boolean) = apply {
|
||||
this.isDraggable = draggable
|
||||
}
|
||||
|
||||
fun isDismissOnTouchOutside(flag: Boolean) = apply {
|
||||
this.isDismissOnTouchOutside = flag
|
||||
}
|
||||
|
||||
fun attachToActivity(activity: Activity) = apply {
|
||||
this.attachToActivity = activity
|
||||
}
|
||||
|
||||
fun offsetX(x: Int) = apply {
|
||||
this.offsetY = x
|
||||
}
|
||||
|
||||
fun offsetY(y: Int) = apply {
|
||||
this.offsetY = y
|
||||
}
|
||||
|
||||
fun width(w: Int) = apply {
|
||||
this.width = w
|
||||
}
|
||||
|
||||
fun height(h: Int) = apply {
|
||||
this.height = h
|
||||
}
|
||||
|
||||
fun gravityInActivity(gravity: Int) = apply {
|
||||
this.gravityInActivity = gravity
|
||||
}
|
||||
|
||||
fun onTouchInterceptorInDrag(block: (childInContent: View, event: MotionEvent) -> Boolean) = apply {
|
||||
this.onTouchInterceptorInDrag = block
|
||||
}
|
||||
|
||||
fun onShowed(block: () -> Unit) = apply {
|
||||
this.onShowed = block
|
||||
}
|
||||
|
||||
fun onDismissed(block: () -> Unit) = apply {
|
||||
this.onDismissed = block
|
||||
}
|
||||
|
||||
fun onViewAttachedToWindow(block: (content: View) -> Unit) = apply {
|
||||
this.onViewAttachedToWindow = block
|
||||
}
|
||||
|
||||
fun build() : MoGoPopWindow {
|
||||
val activity = attachToActivity ?: throw AssertionError("依附的activity不能为空")
|
||||
if (content != null && layoutId != null) {
|
||||
throw AssertionError("两者不能同时设置")
|
||||
}
|
||||
if (content == null && layoutId == null) {
|
||||
throw AssertionError("两者不能都不设置,至少设置一个")
|
||||
}
|
||||
if (content == null) {
|
||||
content = LayoutInflater.from(activity).inflate(layoutId!!, null)
|
||||
}
|
||||
if (content != null && content?.parent != null) {
|
||||
throw AssertionError("内容控件不允许包含父控件.")
|
||||
}
|
||||
return MoGoPopWindow(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.mogo.eagle.core.utilcode.mogo.floating
|
||||
|
||||
import android.annotation.*
|
||||
import android.content.*
|
||||
import android.content.res.*
|
||||
import android.os.*
|
||||
import android.util.*
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import androidx.annotation.*
|
||||
|
||||
abstract class AbsFloatingView: FrameLayout {
|
||||
|
||||
private var mPortraitY = 0f
|
||||
|
||||
private var mWindowWidth: Int = 0
|
||||
|
||||
private var mWindowHeight: Int = 0
|
||||
|
||||
private var mAnimator: AutoMoveEdgeAnimator? = null
|
||||
|
||||
/**
|
||||
* 系统状态栏的高度
|
||||
*/
|
||||
private var mStatusBarHeight: Int = 0
|
||||
|
||||
private var mOldX: Float = 0f
|
||||
|
||||
private var mOldY: Float = 0f
|
||||
|
||||
private var mOldRawX: Float = 0f
|
||||
|
||||
private var mOldRawY: Float = 0f
|
||||
|
||||
companion object {
|
||||
private const val MARGIN_EDGE = 13
|
||||
}
|
||||
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
mStatusBarHeight = getStatusBarHeight()
|
||||
}
|
||||
|
||||
@SuppressLint("InternalInsetResource")
|
||||
private fun getStatusBarHeight(): Int {
|
||||
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
if (resourceId > 0) {
|
||||
return context.resources.getDimensionPixelSize(resourceId)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@CallSuper
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
val layoutId = getLayoutId()
|
||||
if (layoutId == 0) {
|
||||
throw AssertionError("view: ${this::class.java.simpleName}'s getLayoutId() return a invalid resID: 0")
|
||||
}
|
||||
val child = LayoutInflater.from(context).inflate(layoutId, this, true)
|
||||
onChildInflated(child)
|
||||
if (autoMoveToEdge()) {
|
||||
mAnimator = AutoMoveEdgeAnimator()
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onDetachedFromWindow() {
|
||||
if (autoMoveToEdge()) {
|
||||
mAnimator?.stop()
|
||||
}
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
final override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (!isDragEnabled()) {
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
if (event == null) {
|
||||
return false
|
||||
}
|
||||
when(event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
mOldX = x
|
||||
mOldY = y
|
||||
mOldRawX = event.rawX
|
||||
mOldRawY = event.rawY
|
||||
updateSize()
|
||||
if (autoMoveToEdge()) {
|
||||
mAnimator?.stop()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
updateViewPosition(event)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
mPortraitY = 0f
|
||||
if (autoMoveToEdge()) {
|
||||
moveToEdge()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateViewPosition(event: MotionEvent) {
|
||||
var destX = mOldX + event.rawX - mOldRawX
|
||||
if (!autoMoveToEdge()) {
|
||||
if (destX < 0f) {
|
||||
destX = 0f
|
||||
} else {
|
||||
val widthLeft = mWindowWidth - width
|
||||
if (destX > widthLeft) {
|
||||
destX = widthLeft.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
x = destX
|
||||
var destY = mOldY + event.rawY - mOldRawY
|
||||
if (destY < mStatusBarHeight) {
|
||||
destY = mStatusBarHeight.toFloat()
|
||||
}
|
||||
val heightLeft = mWindowHeight - height
|
||||
if (destY > heightLeft) {
|
||||
destY = heightLeft.toFloat()
|
||||
}
|
||||
y = destY
|
||||
}
|
||||
|
||||
private fun moveToEdge() {
|
||||
moveToEdge(isNearestLeft(), context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
}
|
||||
|
||||
private fun moveToEdge(isLeft: Boolean, isLandscape: Boolean) {
|
||||
val moveDistance = if (isLeft) MARGIN_EDGE else mWindowWidth - MARGIN_EDGE
|
||||
if (!isLandscape && mPortraitY != 0f) {
|
||||
y = mPortraitY
|
||||
mPortraitY = 0f
|
||||
}
|
||||
mAnimator?.start(moveDistance.toFloat(), 0f.coerceAtLeast(y).coerceAtMost((mWindowHeight - height).toFloat()))
|
||||
}
|
||||
|
||||
private fun isNearestLeft(): Boolean {
|
||||
val middle = mWindowWidth / 2
|
||||
return x < middle
|
||||
}
|
||||
|
||||
private fun updateSize() {
|
||||
val group = parent as? ViewGroup ?: return
|
||||
mWindowWidth = group.width
|
||||
mWindowHeight = group.height
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (parent != null) {
|
||||
val isLandscape = newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
if (isLandscape) {
|
||||
mPortraitY = y
|
||||
}
|
||||
post {
|
||||
updateSize()
|
||||
moveToEdge(isNearestLeft(), isLandscape)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
abstract fun getLayoutId(): Int
|
||||
|
||||
protected open fun isDragEnabled(): Boolean = true
|
||||
|
||||
protected open fun autoMoveToEdge(): Boolean = false
|
||||
|
||||
protected open fun onChildInflated(child: View) { }
|
||||
|
||||
private fun move(deltaX: Float, deltaY: Float) {
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
}
|
||||
|
||||
private inner class AutoMoveEdgeAnimator: Runnable {
|
||||
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
private var destX: Float = 0f
|
||||
|
||||
private var destY: Float = 0f
|
||||
|
||||
private var startTime: Long = 0L
|
||||
|
||||
override fun run() {
|
||||
if (rootView == null || rootView.parent == null) {
|
||||
return
|
||||
}
|
||||
val progress = 1f.coerceAtMost((SystemClock.elapsedRealtime() - startTime) / 400f)
|
||||
val deltaX = (destX - x) * progress
|
||||
val deltaY = (destY - y) * progress
|
||||
move(deltaX, deltaY)
|
||||
if (progress < 1) {
|
||||
handler.post(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(x: Float, y: Float) {
|
||||
this.destX = x
|
||||
this.destY = y
|
||||
startTime = SystemClock.elapsedRealtime()
|
||||
handler.post(this)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
handler.removeCallbacks(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<item name="tag_click_time" type="id" />
|
||||
<item name="view_lifecycle_owner" type="id" />
|
||||
<item name="mogo_pop_window" type="id" />
|
||||
</resources>
|
||||
Reference in New Issue
Block a user