android调试助手基于recyclerView
效果图如下
一 、自定义View
fun Context.dip(value: Float): Int = (value * resources.displayMetrics.density).toInt()
fun Context.dip(value: Int): Int = dip(value.toFloat())
fun Context.sp(value: Float): Int = (value * resources.displayMetrics.scaledDensity).toInt()
fun Context.sp(value: Int): Int = sp(value.toFloat())
fun Context.width(): Int = resources.displayMetrics.widthPixels
fun Context.height(): Int = resources.displayMetrics.heightPixels
class DebugView constructor(context: Context, attrs: AttributeSet? = null, defStyleId: Int = 0) :
ConstraintLayout(context, attrs, defStyleId), View.OnClickListener,
CoroutineScope by MainScope() {
private val container: FrameLayout
private val recyclerView: RecyclerView
private val navigationBackView: ImageView
private val titleView: TextView
private val toggleView: ImageView
private val viewController: ViewController
init {
background = GradientDrawable().apply {
setColor(Color.WHITE)
cornerRadius = context.dip(5).toFloat()
}
inflate(context, R.layout.debug_layout_main, this)
navigationBackView = findViewById(R.id.back)
container = findViewById(R.id.container)
recyclerView = findViewById(R.id.recycler)
titleView = findViewById(R.id.title)
toggleView = findViewById(R.id.toggle)
viewController = ViewController(container)
viewController.onStackChangeListener = object : ViewController.OnStackChangeListener {
override fun onStackChanged(controller: ViewController) {
if (controller.isRoot()) {
navigationBackView.visibility = View.GONE
} else {
navigationBackView.visibility = View.VISIBLE
}
}
}
navigationBackView.setOnClickListener {
if (!viewController.isRoot()) {
viewController.pop()
}
}
val items = buildItems()
val adapter = DebugAdapter(context, items)
adapter.registerFactory(ExitItem::class.java, ExitItem.Factory)
recyclerView.adapter = adapter
val spanCount = 2
recyclerView.layoutManager = GridLayoutManager(context, spanCount).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (items.size - 1 == position) {
spanCount
} else {
1
}
}
}
}
recyclerView.addItemDecoration(SimpleDividerDecoration())
toggleView.clipToOutline = true
toggleView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setOval(0, 0, view.width, view.height)
}
}
toggleView.setBackgroundColor(Color.GREEN)
toggleView.setOnClickListener(this)
}
// 右上角的绿色开关 隐藏和显示
private fun toggle() {
if (container.visibility == View.VISIBLE) {
container.visibility = View.GONE
toggleView.setBackgroundColor(Color.BLUE)
} else {
container.visibility = View.VISIBLE
toggleView.setBackgroundColor(Color.GREEN)
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.toggle -> toggle()
}
}
private fun buildItems(): List<DebugItem> {
val items: MutableList<DebugItem> = mutableListOf()
items.add(DefaultDebugItem("ADB相关") {
// TODO 添加点击item的操作 })
items.add(ExitItem(context))
return items
}
}
二、布局XML
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:src="@drawable/navigation_back_black_18dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/container"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/title"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="调试助手"
android:textColor="#333"
app:layout_constraintBottom_toTopOf="@id/container"
app:layout_constraintLeft_toRightOf="@id/back"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="140dp"
app:layout_goneMarginStart="12dp" />
<ImageView
android:id="@+id/toggle"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintRight_toRightOf="@id/title"
app:layout_constraintTop_toTopOf="@id/title" />
<FrameLayout
android:id="@+id/container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintWidth_min="250dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</merge>
三、工具类和Adapter
ViewController
class ViewController(private val container: FrameLayout) {
interface OnStackChangeListener {
fun onStackChanged(controller: ViewController)
}
var onStackChangeListener: OnStackChangeListener? = null
fun isRoot(): Boolean {
return container.childCount <= 1
}
fun push(view: View) {
if (container.indexOfChild(view) != -1) {
return
}
container.addView(view)
onStackChangeListener?.onStackChanged(this)
}
fun pop() {
val top = container.getChildAt(container.childCount - 1) ?: return
container.removeView(top)
onStackChangeListener?.onStackChanged(this)
}
}
DebugAdapter
class DebugAdapter(
private val context: Context,
private val items: List<DebugItem>
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var baseViewType = 0
private val itemTypes = mutableMapOf<String, Int>()
private fun getViewTypeOf(item: DebugItem): Int = getViewTypeOf(item.javaClass)
private fun getViewTypeOf(clazz: Class<out DebugItem>): Int {
return itemTypes[clazz.name] ?: run {
val newType = baseViewType++
itemTypes[clazz.name] = newType
newType
}
}
private val factories = mutableMapOf<Int, DebugItem.Factory>()
private fun getFactoryOf(viewType: Int): DebugItem.Factory {
return factories[viewType] ?: DebugItem.DefaultFactory
}
fun registerFactory(item: Class<out DebugItem>, factory: DebugItem.Factory) {
factories[getViewTypeOf(item)] = factory
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val factory = getFactoryOf(viewType)
return object : RecyclerView.ViewHolder(factory.createView(parent)) {}
}
override fun getItemCount(): Int {
return items.size
}
override fun getItemViewType(position: Int): Int {
return getViewTypeOf(items[position])
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
item.render(holder.itemView)
if (item is View.OnClickListener) {
holder.itemView.setOnClickListener(item)
}
}
}
RecyclerView分割线
class SimpleDividerDecoration : RecyclerView.ItemDecoration() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0xFFF5F5F5.toInt()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildLayoutPosition(view)
val bottom = if (position == state.itemCount - 1) 0 else view.context.dip(1)
outRect.set(0, 0, 0, bottom)
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
(0 until parent.childCount - 1).forEach {
val child = parent.getChildAt(it)
val params = child.layoutParams as ViewGroup.MarginLayoutParams
val left = child.left - params.leftMargin
val right = child.right + params.rightMargin
val top = child.bottom + params.bottomMargin
val bottom = top + parent.context.dip(1)
c.drawRect(
left.toFloat(),
top.toFloat(),
right.toFloat(),
bottom.toFloat(),
paint
)
}
}
}
DebugItem
abstract class DebugItem(val name: String) {
interface Factory {
fun createView(parent: ViewGroup): View
}
abstract fun render(view: View)
companion object {
val DefaultFactory = object : Factory {
override fun createView(parent: ViewGroup): TextView {
return TextView(parent.context).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
textSize = 16F
gravity = Gravity.CENTER
setTextColor(0xFF333333.toInt())
setBackgroundResource(R.drawable.debug_item_background)
setPadding(context.dip(16), context.dip(16), context.dip(16), context.dip(16))
}
}
}
}
}
open class DefaultDebugItem(
name: String,
private val onClick: ((View) -> Unit)? = null
) :
DebugItem(name), View.OnClickListener {
override fun render(view: View) {
(view as TextView).text = name
}
override fun onClick(v: View) {
this.onClick?.invoke(v)
}
}
class ExitItem(val context: Context) : DefaultDebugItem("关闭浮窗", onClick = {
context.stopService(Intent(context, DebugService::class.java))
}) {
companion object Factory :
DebugItem.Factory {
override fun createView(parent: ViewGroup): TextView {
return TextView(parent.context).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
textSize = 16F
gravity = Gravity.CENTER
setTextColor(0xFFFFFFFF.toInt())
setBackgroundResource(R.drawable.debug_item_background_red)
setPadding(context.dip(16), context.dip(16), context.dip(16), context.dip(16))
}
}
}
}
ScrollTouchListener
class ScrollTouchListener(
private val wm: WindowManager
) : View.OnTouchListener {
private var x = 0
private var y = 0
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
x = motionEvent.rawX.toInt()
y = motionEvent.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
val nowX = motionEvent.rawX.toInt()
val nowY = motionEvent.rawY.toInt()
val movedX = nowX - x
val movedY = nowY - y
x = nowX
y = nowY
(view.layoutParams as? WindowManager.LayoutParams)?.apply {
x += movedX
y += movedY
wm.updateViewLayout(view, this)
}
}
else -> {
}
}
return false
}
}
四、添加view
private val wm by lazy {
getSystemService(WINDOW_SERVICE) as WindowManager
}
private val entrance by lazy {
DebugView(applicationContext)
}
override fun onCreate() {
super.onCreate()
wm.addView(
entrance, WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
},
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)
)
entrance.setOnTouchListener(ScrollTouchListener(wm))
}