├── .gitignore ├── README.md ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── angcyo │ │ └── hover │ │ └── item │ │ └── decoration │ │ ├── HoverItemDecoration.kt │ │ ├── MainActivity.kt │ │ ├── RBaseViewHolder.kt │ │ ├── RecyclerEx.kt │ │ └── dsl │ │ ├── DslAdapter.kt │ │ ├── DslAdapterItem.kt │ │ └── DslRecyclerScroll.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable-xhdpi │ └── peppq.jpg │ ├── drawable │ ├── ic_launcher_background.xml │ ├── main_selector.xml │ ├── main_selector_ripple.xml │ ├── transparent_selector.xml │ └── transparent_selector_ripple.xml │ ├── layout │ ├── activity_main.xml │ ├── content_main.xml │ ├── item_image.xml │ ├── item_image_little.xml │ └── item_text.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.apk 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | build 9 | */build 10 | /captures 11 | .externalNativeBuild 12 | /.idea/ 13 | /gradle/wrapper/gradle-wrapper.properties 14 | gradle/ 15 | gradlew 16 | gradlew.bat 17 | /RLibrary2 18 | /RLibrary 19 | /apk 20 | /RTbs 21 | /RJpush 22 | /jpush 23 | *.log 24 | /RRealm 25 | 26 | /httpserver 27 | /RHttpServer 28 | /ble 29 | /umeng 30 | /tbs 31 | /uikit 32 | /uikitex -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HoverItemDecoration 2 | Kotlin--›超轻量RecyclerView悬停效果(带touch点击事件) 3 | 4 | # 特性 5 | 1. 支持仅绘制分割线 不带`Touch事件` 不带`Drawable状态`控制. 也就是没有任何交互. 6 | 2. 支持仅带`Touch事件` 不带`Drawable状态`控制. 7 | 3. 极致体验 带`Touch事件` 带`Drawable状态`控制. 堪称完美! 8 | 9 | # 使用方式 10 | 11 | 额... 12 | 13 | ```kotlin 14 | /**极致体验, 想哪悬停, 就哪悬停*/ 15 | itemIsHover = true 16 | ``` 17 | 18 | 封装的好 使用起来就是一个开关. 19 | 20 | 具体, 还是看代码吧; 21 | 22 | 23 | --- 24 | 25 | ```kotlin 26 | HoverItemDecoration().attachToRecyclerView(recyclerView) 27 | ``` 28 | 29 | ```kotlin 30 | HoverItemDecoration().attachToRecyclerView(recyclerView) { 31 | enableTouchEvent = true 32 | enableDrawableState = true 33 | } 34 | ``` 35 | ```kotlin 36 | HoverItemDecoration().attachToRecyclerView(recyclerView) { 37 | enableTouchEvent = false 38 | enableDrawableState = false 39 | } 40 | ``` 41 | 42 | **更多配置** 43 | 44 | 参考[HoverCallback]类: 45 | 46 | ```kotlin 47 | class HoverCallback { 48 | 49 | /**激活touch手势*/ 50 | var enableTouchEvent = true 51 | 52 | /**激活drawable点击效果, 此功能需要先 激活 touch 手势*/ 53 | var enableDrawableState = enableTouchEvent 54 | 55 | /** 56 | * 当前的 位置 是否有 悬浮分割线 57 | * */ 58 | var haveOverDecoration: (adapter: RecyclerView.Adapter<*>, adapterPosition: Int) -> Boolean = 59 | { adapter, adapterPosition -> 60 | if (adapter is DslAdapter) { 61 | adapter.getItemData(adapterPosition).itemIsHover 62 | } else { 63 | decorationOverLayoutType.invoke(adapter, adapterPosition) > 0 64 | } 65 | } 66 | 67 | /** 68 | * 根据 位置, 返回对应分割线的布局类型, 小于0, 不绘制 69 | * 70 | * @see RecyclerView.Adapter.getItemViewType 71 | * */ 72 | var decorationOverLayoutType: (adapter: RecyclerView.Adapter<*>, adapterPosition: Int) -> Int = 73 | { adapter, adapterPosition -> 74 | if (adapter is DslAdapter) { 75 | adapter.getItemViewType(adapterPosition) 76 | } else { 77 | -1 78 | } 79 | } 80 | 81 | /** 82 | * 判断2个分割线是否相同, 不同的分割线, 才会悬停, 相同的分割线只会绘制一条. 83 | * */ 84 | var isOverDecorationSame: ( 85 | adapter: RecyclerView.Adapter, 86 | nowAdapterPosition: Int, nextAdapterPosition: Int 87 | ) -> Boolean = 88 | { _, _, _ -> 89 | false 90 | } 91 | 92 | /** 93 | * 创建 分割线 视图 94 | * */ 95 | var createDecorationOverView: ( 96 | recyclerView: RecyclerView, 97 | adapter: RecyclerView.Adapter, 98 | overAdapterPosition: Int 99 | ) -> RecyclerView.ViewHolder = { recyclerView, adapter, overAdapterPosition -> 100 | 101 | //拿到分割线对应的itemType 102 | val layoutType = decorationOverLayoutType.invoke(adapter, overAdapterPosition) 103 | 104 | //复用adapter的机制, 创建View 105 | val holder = adapter.createViewHolder(recyclerView, layoutType) 106 | 107 | //注意这里的position 108 | adapter.bindViewHolder(holder, overAdapterPosition) 109 | 110 | //测量view 111 | measureHoverView.invoke(recyclerView, holder.itemView) 112 | 113 | holder 114 | } 115 | 116 | /**自定义layout的分割线, 不使用 adapter中的xml*/ 117 | val customDecorationOverView: ( 118 | recyclerView: RecyclerView, 119 | adapter: RecyclerView.Adapter, 120 | overAdapterPosition: Int 121 | ) -> RecyclerView.ViewHolder = { recyclerView, adapter, overAdapterPosition -> 122 | 123 | //拿到分割线对应的itemType 124 | val layoutType = decorationOverLayoutType.invoke(adapter, overAdapterPosition) 125 | 126 | val itemView = LayoutInflater.from(recyclerView.context).inflate(layoutType, recyclerView, false) 127 | 128 | val holder = RBaseViewHolder(itemView) 129 | 130 | //注意这里的position 131 | adapter.bindViewHolder(holder, overAdapterPosition) 132 | 133 | //测量view 134 | measureHoverView.invoke(recyclerView, holder.itemView) 135 | 136 | holder 137 | } 138 | 139 | /** 140 | * 测量 View, 确定宽高和绘制坐标 141 | * */ 142 | var measureHoverView: (parent: RecyclerView, hoverView: View) -> Unit = { parent, hoverView -> 143 | hoverView.apply { 144 | val params = layoutParams 145 | 146 | val widthSize: Int 147 | val widthMode: Int 148 | when (params.width) { 149 | -1 -> { 150 | widthSize = parent.measuredWidth 151 | widthMode = View.MeasureSpec.EXACTLY 152 | } 153 | else -> { 154 | widthSize = parent.measuredWidth 155 | widthMode = View.MeasureSpec.AT_MOST 156 | } 157 | } 158 | 159 | val heightSize: Int 160 | val heightMode: Int 161 | when (params.height) { 162 | -1 -> { 163 | heightSize = parent.measuredWidth 164 | heightMode = View.MeasureSpec.EXACTLY 165 | } 166 | else -> { 167 | heightSize = parent.measuredWidth 168 | heightMode = View.MeasureSpec.AT_MOST 169 | } 170 | } 171 | 172 | //标准方法1 173 | measure( 174 | View.MeasureSpec.makeMeasureSpec(widthSize, widthMode), 175 | View.MeasureSpec.makeMeasureSpec(heightSize, heightMode) 176 | ) 177 | //标准方法2 178 | layout(0, 0, measuredWidth, measuredHeight) 179 | 180 | //标准方法3 181 | //draw(canvas) 182 | } 183 | } 184 | 185 | /** 186 | * 绘制分割线, 请不要使用 foreground 属性. 187 | * */ 188 | var drawOverDecoration: ( 189 | canvas: Canvas, 190 | paint: Paint, 191 | viewHolder: RecyclerView.ViewHolder, 192 | overRect: Rect 193 | ) -> Unit = 194 | { canvas, paint, viewHolder, overRect -> 195 | 196 | canvas.save() 197 | canvas.translate(overRect.left.toFloat(), overRect.top.toFloat()) 198 | 199 | viewHolder.itemView.draw(canvas) 200 | 201 | if (enableDrawShadow) { 202 | drawOverShadowDecoration.invoke(canvas, paint, viewHolder, overRect) 203 | } 204 | 205 | canvas.restore() 206 | } 207 | 208 | /**是否激活阴影绘制*/ 209 | var enableDrawShadow = true 210 | 211 | /** 212 | * 绘制分割线下面的阴影, 或者其他而外的信息 213 | * */ 214 | var drawOverShadowDecoration: ( 215 | canvas: Canvas, 216 | paint: Paint, 217 | viewHolder: RecyclerView.ViewHolder, 218 | overRect: Rect 219 | ) -> Unit = 220 | { canvas, paint, viewHolder, overRect -> 221 | 222 | if (overRect.top == 0) { 223 | //分割线完全显示的情况下, 才绘制阴影 224 | val shadowTop = overRect.bottom.toFloat() 225 | val shadowHeight = 4 * dp 226 | 227 | paint.shader = LinearGradient( 228 | 0f, shadowTop, 0f, 229 | shadowTop + shadowHeight, 230 | intArrayOf( 231 | Color.parseColor("#40000000"), 232 | Color.TRANSPARENT /*Color.parseColor("#40000000")*/ 233 | ), 234 | null, Shader.TileMode.CLAMP 235 | ) 236 | 237 | //绘制阴影 238 | canvas.drawRect( 239 | overRect.left.toFloat(), 240 | shadowTop, 241 | overRect.right.toFloat(), 242 | shadowTop + shadowHeight, 243 | paint 244 | ) 245 | } 246 | } 247 | } 248 | ``` 249 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.31' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.4.0' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.angcyo.hover.item.decoration" 11 | minSdkVersion 21 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 28 | implementation 'com.android.support:appcompat-v7:28.0.0' 29 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 30 | implementation 'com.android.support:design:28.0.0' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 33 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 34 | } 35 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/HoverItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import android.graphics.* 7 | import android.support.v7.widget.GridLayoutManager 8 | import android.support.v7.widget.RecyclerView 9 | import android.view.* 10 | import android.widget.FrameLayout 11 | import com.angcyo.hover.item.decoration.dsl.DslAdapter 12 | 13 | /** 14 | * 15 | * Email:angcyo@126.com 16 | * @author angcyo 17 | * @date 2019/05/08 18 | * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. 19 | */ 20 | 21 | open class HoverItemDecoration : RecyclerView.ItemDecoration() { 22 | internal var recyclerView: RecyclerView? = null 23 | internal var hoverCallback: HoverCallback? = null 24 | internal var isDownInHoverItem = false 25 | 26 | /**分割线追加在的容器*/ 27 | internal var windowContent: ViewGroup? = null 28 | 29 | /**需要移除的分割线*/ 30 | internal val removeViews = arrayListOf() 31 | 32 | val cancelEvent = Runnable { 33 | overViewHolder?.apply { 34 | 35 | itemView.dispatchTouchEvent( 36 | MotionEvent.obtain( 37 | nowTime(), 38 | nowTime(), 39 | MotionEvent.ACTION_UP, 40 | 0f, 41 | 0f, 42 | 0 43 | ) 44 | ) 45 | } 46 | } 47 | 48 | internal val paint: Paint by lazy { 49 | Paint(Paint.ANTI_ALIAS_FLAG) 50 | } 51 | 52 | private val itemTouchListener = object : RecyclerView.SimpleOnItemTouchListener() { 53 | override fun onInterceptTouchEvent(recyclerView: RecyclerView, event: MotionEvent): Boolean { 54 | val action = event.actionMasked 55 | if (action == MotionEvent.ACTION_DOWN) { 56 | isDownInHoverItem = overDecorationRect.contains(event.x.toInt(), event.y.toInt()) 57 | } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 58 | isDownInHoverItem = false 59 | } 60 | 61 | if (isDownInHoverItem) { 62 | onTouchEvent(recyclerView, event) 63 | } 64 | 65 | return isDownInHoverItem 66 | } 67 | 68 | override fun onTouchEvent(recyclerView: RecyclerView, event: MotionEvent) { 69 | if (isDownInHoverItem) { 70 | overViewHolder?.apply { 71 | if (hoverCallback?.enableDrawableState == true) { 72 | if (event.actionMasked == MotionEvent.ACTION_DOWN) { 73 | //有些时候, 可能收不到 up/cancel 事件, 延时发送cancel事件, 解决一些 界面drawable的bug 74 | recyclerView.postDelayed(cancelEvent, 160L) 75 | } else { 76 | recyclerView.removeCallbacks(cancelEvent) 77 | } 78 | 79 | //一定要调用dispatchTouchEvent, 否则ViewGroup里面的子View, 不会响应touchEvent 80 | itemView.dispatchTouchEvent(event) 81 | if (itemView is ViewGroup) { 82 | if ((itemView as ViewGroup).onInterceptTouchEvent(event)) { 83 | itemView.onTouchEvent(event) 84 | } 85 | } else { 86 | itemView.onTouchEvent(event) 87 | } 88 | } else { 89 | //没有Drawable状态, 需要手动控制touch事件, 因为系统已经管理不了 itemView了 90 | if (event.actionMasked == MotionEvent.ACTION_UP) { 91 | itemView.performClick() 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | private val attachStateChangeListener = object : View.OnAttachStateChangeListener { 100 | override fun onViewDetachedFromWindow(view: View?) { 101 | removeAllHoverView() 102 | } 103 | 104 | override fun onViewAttachedToWindow(view: View?) { 105 | 106 | } 107 | } 108 | 109 | private val scrollListener = object : RecyclerView.OnScrollListener() { 110 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 111 | super.onScrollStateChanged(recyclerView, newState) 112 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 113 | //滚动状态结束 114 | removeAllHoverView() 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * 调用此方法, 安装悬浮分割线 121 | * */ 122 | fun attachToRecyclerView(recyclerView: RecyclerView?, init: HoverCallback.() -> Unit = {}) { 123 | hoverCallback = HoverCallback() 124 | hoverCallback?.init() 125 | 126 | if (this.recyclerView !== recyclerView) { 127 | if (this.recyclerView != null) { 128 | this.destroyCallbacks() 129 | } 130 | 131 | this.recyclerView = recyclerView 132 | if (recyclerView != null) { 133 | this.setupCallbacks() 134 | } 135 | 136 | (recyclerView?.context as? Activity)?.apply { 137 | windowContent = window.findViewById(Window.ID_ANDROID_CONTENT) 138 | } 139 | } 140 | } 141 | 142 | /**卸载分割线*/ 143 | fun detachedFromRecyclerView() { 144 | hoverCallback = null 145 | 146 | if (this.recyclerView != null) { 147 | this.destroyCallbacks() 148 | } 149 | isDownInHoverItem = false 150 | windowContent = null 151 | recyclerView = null 152 | } 153 | 154 | private fun setupCallbacks() { 155 | this.recyclerView?.apply { 156 | addItemDecoration(this@HoverItemDecoration) 157 | if (hoverCallback?.enableTouchEvent == true) { 158 | addOnItemTouchListener(itemTouchListener) 159 | } 160 | addOnAttachStateChangeListener(attachStateChangeListener) 161 | addOnScrollListener(scrollListener) 162 | } 163 | } 164 | 165 | private fun destroyCallbacks() { 166 | this.recyclerView?.apply { 167 | removeItemDecoration(this@HoverItemDecoration) 168 | removeOnItemTouchListener(itemTouchListener) 169 | removeOnAttachStateChangeListener(attachStateChangeListener) 170 | removeOnScrollListener(scrollListener) 171 | } 172 | removeAllHoverView() 173 | } 174 | 175 | private fun removeAllHoverView() { 176 | removeViews.forEach { 177 | (it.parent as? ViewGroup)?.removeView(it) 178 | } 179 | 180 | removeViews.clear() 181 | } 182 | 183 | /** 184 | * 从Activity移除悬浮view 185 | * */ 186 | private fun removeHoverView() { 187 | overViewHolder?.itemView?.apply { 188 | dispatchTouchEvent(MotionEvent.obtain(nowTime(), nowTime(), MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) 189 | //(parent as? ViewGroup)?.removeView(this) 190 | 191 | removeViews.add(this) 192 | } 193 | } 194 | 195 | /** 196 | * 添加悬浮view 到 Activity, 目的是为了 系统接管 悬浮View的touch事件以及drawable的state 197 | * */ 198 | private fun addHoverView(view: View) { 199 | if (view.parent == null) { 200 | windowContent?.addView( 201 | view, 0, 202 | FrameLayout.LayoutParams(overDecorationRect.width(), overDecorationRect.height()).apply { 203 | val viewRect = recyclerView?.getViewRect() 204 | 205 | leftMargin = overDecorationRect.left + (viewRect?.left ?: 0) 206 | topMargin = overDecorationRect.top + (viewRect?.top ?: 0) 207 | } 208 | ) 209 | } 210 | } 211 | 212 | override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { 213 | if (state.isPreLayout || state.willRunSimpleAnimations()) { 214 | return 215 | } 216 | 217 | checkOverDecoration(parent) 218 | 219 | overViewHolder?.let { 220 | if (!overDecorationRect.isEmpty) { 221 | 222 | //L.d("...onDrawOverDecoration...") 223 | hoverCallback?.apply { 224 | if (enableTouchEvent && enableDrawableState) { 225 | addHoverView(it.itemView) 226 | } 227 | 228 | drawOverDecoration.invoke(canvas, paint, it, overDecorationRect) 229 | } 230 | } 231 | } 232 | } 233 | 234 | private fun childViewHolder(parent: RecyclerView, childIndex: Int): RecyclerView.ViewHolder? { 235 | if (parent.childCount > childIndex) { 236 | return parent.findContainingViewHolder(parent.getChildAt(childIndex)) 237 | } 238 | return null 239 | } 240 | 241 | /**当前悬浮的分割线, 如果有.*/ 242 | internal var overViewHolder: RecyclerView.ViewHolder? = null 243 | 244 | /**当前悬浮分割线的坐标.*/ 245 | val overDecorationRect = Rect() 246 | /**下一个悬浮分割线的坐标.*/ 247 | internal val nextDecorationRect = Rect() 248 | /**分割线的所在位置*/ 249 | var overAdapterPosition = RecyclerView.NO_POSITION 250 | 251 | private var tempRect = Rect() 252 | /** 253 | * 核心方法, 用来实时监测界面上 需要浮动的 分割线. 254 | * */ 255 | internal fun checkOverDecoration(parent: RecyclerView) { 256 | childViewHolder(parent, 0)?.let { viewHolder -> 257 | val firstChildAdapterPosition = viewHolder.adapterPosition 258 | 259 | if (firstChildAdapterPosition != RecyclerView.NO_POSITION) { 260 | 261 | parent.adapter?.let { adapter -> 262 | hoverCallback?.let { callback -> 263 | 264 | var firstChildHaveOver = callback.haveOverDecoration.invoke(adapter, firstChildAdapterPosition) 265 | 266 | //第一个child, 需要分割线的 position 的位置 267 | var firstChildHaveOverPosition = firstChildAdapterPosition 268 | 269 | if (!firstChildHaveOver) { 270 | //第一个child没有分割线, 查找之前最近有分割线的position 271 | val findOverPrePosition = findOverPrePosition(adapter, firstChildAdapterPosition) 272 | if (findOverPrePosition != RecyclerView.NO_POSITION) { 273 | //找到了最近的分割线 274 | firstChildHaveOver = true 275 | 276 | firstChildHaveOverPosition = findOverPrePosition 277 | } 278 | } 279 | 280 | if (firstChildHaveOver) { 281 | 282 | val overStartPosition = findOverStartPosition(adapter, firstChildHaveOverPosition) 283 | 284 | if (overStartPosition == RecyclerView.NO_POSITION) { 285 | clearOverDecoration() 286 | return 287 | } else { 288 | firstChildHaveOverPosition = overStartPosition 289 | } 290 | 291 | //创建第一个位置的child 需要分割线 292 | val firstViewHolder = 293 | callback.createDecorationOverView.invoke( 294 | parent, 295 | adapter, 296 | firstChildHaveOverPosition 297 | ) 298 | 299 | val overView = firstViewHolder.itemView 300 | tempRect.set(overView.left, overView.top, overView.right, overView.bottom) 301 | 302 | val nextViewHolder = childViewHolder(parent, findGridNextChildIndex()) 303 | if (nextViewHolder != null) { 304 | //紧挨着的下一个child也有分割线, 监测是否需要上推 305 | 306 | if (callback.haveOverDecoration.invoke(adapter, nextViewHolder.adapterPosition) && 307 | !callback.isOverDecorationSame.invoke( 308 | adapter, 309 | firstChildAdapterPosition, 310 | nextViewHolder.adapterPosition 311 | ) 312 | ) { 313 | //不同的分割线, 实现上推效果 314 | if (nextViewHolder.itemView.top < tempRect.height()) { 315 | tempRect.offsetTo( 316 | 0, 317 | nextViewHolder.itemView.top - tempRect.height() 318 | ) 319 | } 320 | } 321 | } 322 | 323 | if (firstChildHaveOverPosition == firstChildAdapterPosition && viewHolder.itemView.top >= 0 /*考虑分割线*/) { 324 | //第一个child, 正好是 分割线的开始位置 325 | clearOverDecoration() 326 | } else { 327 | if (overAdapterPosition != firstChildHaveOverPosition) { 328 | clearOverDecoration() 329 | 330 | overViewHolder = firstViewHolder 331 | overDecorationRect.set(tempRect) 332 | 333 | overAdapterPosition = firstChildHaveOverPosition 334 | } else if (overDecorationRect != tempRect) { 335 | overDecorationRect.set(tempRect) 336 | } 337 | } 338 | } else { 339 | //当前位置不需要分割线 340 | clearOverDecoration() 341 | } 342 | } 343 | } 344 | } 345 | } 346 | } 347 | 348 | /** 349 | * 查找GridLayoutManager中, 下一个具有全屏样式的child索引 350 | * */ 351 | internal fun findGridNextChildIndex(): Int { 352 | var result = 1 353 | recyclerView?.layoutManager?.apply { 354 | if (this is GridLayoutManager) { 355 | 356 | for (i in 1 until recyclerView!!.childCount) { 357 | childViewHolder(recyclerView!!, i)?.let { 358 | if (it.adapterPosition != RecyclerView.NO_POSITION) { 359 | if (spanSizeLookup?.getSpanSize(it.adapterPosition) == this.spanCount) { 360 | result = i 361 | 362 | return result 363 | } 364 | } 365 | } 366 | } 367 | } 368 | } 369 | return result 370 | } 371 | 372 | fun clearOverDecoration() { 373 | overDecorationRect.clear() 374 | nextDecorationRect.clear() 375 | removeHoverView() 376 | overViewHolder = null 377 | overAdapterPosition = RecyclerView.NO_POSITION 378 | } 379 | 380 | /** 381 | * 查找指定位置类型相同的分割线, 最开始的adapterPosition 382 | * */ 383 | internal fun findOverStartPosition( 384 | adapter: RecyclerView.Adapter, 385 | adapterPosition: Int 386 | ): Int { 387 | var result = adapterPosition 388 | for (i in adapterPosition - 1 downTo 0) { 389 | if (i == 0) { 390 | if (hoverCallback!!.isOverDecorationSame(adapter, adapterPosition, i)) { 391 | result = i 392 | } 393 | break 394 | } else if (!hoverCallback!!.isOverDecorationSame(adapter, adapterPosition, i)) { 395 | result = i + 1 396 | break 397 | } 398 | } 399 | 400 | if (result == 0) { 401 | hoverCallback?.let { 402 | if (!it.haveOverDecoration.invoke(adapter, result)) { 403 | result = RecyclerView.NO_POSITION 404 | } 405 | } 406 | } 407 | 408 | return result 409 | } 410 | 411 | /** 412 | * 查找指定位置 没有分割线时, 最前出现分割线的adapterPosition 413 | * */ 414 | internal fun findOverPrePosition(adapter: RecyclerView.Adapter<*>, adapterPosition: Int): Int { 415 | var result = RecyclerView.NO_POSITION 416 | for (i in adapterPosition - 1 downTo 0) { 417 | if (hoverCallback!!.haveOverDecoration.invoke(adapter, i)) { 418 | result = i 419 | break 420 | } 421 | } 422 | return result 423 | } 424 | 425 | class HoverCallback { 426 | 427 | /**激活touch手势*/ 428 | var enableTouchEvent = true 429 | 430 | /**激活drawable点击效果, 此功能需要先 激活 touch 手势*/ 431 | var enableDrawableState = enableTouchEvent 432 | 433 | /** 434 | * 当前的 位置 是否有 悬浮分割线 435 | * */ 436 | var haveOverDecoration: (adapter: RecyclerView.Adapter<*>, adapterPosition: Int) -> Boolean = 437 | { adapter, adapterPosition -> 438 | if (adapter is DslAdapter) { 439 | adapter.getItemData(adapterPosition).itemIsHover 440 | } else { 441 | decorationOverLayoutType.invoke(adapter, adapterPosition) > 0 442 | } 443 | } 444 | 445 | /** 446 | * 根据 位置, 返回对应分割线的布局类型, 小于0, 不绘制 447 | * 448 | * @see RecyclerView.Adapter.getItemViewType 449 | * */ 450 | var decorationOverLayoutType: (adapter: RecyclerView.Adapter<*>, adapterPosition: Int) -> Int = 451 | { adapter, adapterPosition -> 452 | if (adapter is DslAdapter) { 453 | adapter.getItemViewType(adapterPosition) 454 | } else { 455 | -1 456 | } 457 | } 458 | 459 | /** 460 | * 判断2个分割线是否相同, 不同的分割线, 才会悬停, 相同的分割线只会绘制一条. 461 | * */ 462 | var isOverDecorationSame: ( 463 | adapter: RecyclerView.Adapter, 464 | nowAdapterPosition: Int, nextAdapterPosition: Int 465 | ) -> Boolean = 466 | { _, _, _ -> 467 | false 468 | } 469 | 470 | /** 471 | * 创建 分割线 视图 472 | * */ 473 | var createDecorationOverView: ( 474 | recyclerView: RecyclerView, 475 | adapter: RecyclerView.Adapter, 476 | overAdapterPosition: Int 477 | ) -> RecyclerView.ViewHolder = { recyclerView, adapter, overAdapterPosition -> 478 | 479 | //拿到分割线对应的itemType 480 | val layoutType = decorationOverLayoutType.invoke(adapter, overAdapterPosition) 481 | 482 | //复用adapter的机制, 创建View 483 | val holder = adapter.createViewHolder(recyclerView, layoutType) 484 | 485 | //注意这里的position 486 | adapter.bindViewHolder(holder, overAdapterPosition) 487 | 488 | //测量view 489 | measureHoverView.invoke(recyclerView, holder.itemView) 490 | 491 | holder 492 | } 493 | 494 | /**自定义layout的分割线, 不使用 adapter中的xml*/ 495 | val customDecorationOverView: ( 496 | recyclerView: RecyclerView, 497 | adapter: RecyclerView.Adapter, 498 | overAdapterPosition: Int 499 | ) -> RecyclerView.ViewHolder = { recyclerView, adapter, overAdapterPosition -> 500 | 501 | //拿到分割线对应的itemType 502 | val layoutType = decorationOverLayoutType.invoke(adapter, overAdapterPosition) 503 | 504 | val itemView = LayoutInflater.from(recyclerView.context).inflate(layoutType, recyclerView, false) 505 | 506 | val holder = RBaseViewHolder(itemView) 507 | 508 | //注意这里的position 509 | adapter.bindViewHolder(holder, overAdapterPosition) 510 | 511 | //测量view 512 | measureHoverView.invoke(recyclerView, holder.itemView) 513 | 514 | holder 515 | } 516 | 517 | /** 518 | * 测量 View, 确定宽高和绘制坐标 519 | * */ 520 | var measureHoverView: (parent: RecyclerView, hoverView: View) -> Unit = { parent, hoverView -> 521 | hoverView.apply { 522 | val params = layoutParams 523 | 524 | val widthSize: Int 525 | val widthMode: Int 526 | when (params.width) { 527 | -1 -> { 528 | widthSize = parent.measuredWidth 529 | widthMode = View.MeasureSpec.EXACTLY 530 | } 531 | else -> { 532 | widthSize = parent.measuredWidth 533 | widthMode = View.MeasureSpec.AT_MOST 534 | } 535 | } 536 | 537 | val heightSize: Int 538 | val heightMode: Int 539 | when (params.height) { 540 | -1 -> { 541 | heightSize = parent.measuredWidth 542 | heightMode = View.MeasureSpec.EXACTLY 543 | } 544 | else -> { 545 | heightSize = parent.measuredWidth 546 | heightMode = View.MeasureSpec.AT_MOST 547 | } 548 | } 549 | 550 | //标准方法1 551 | measure( 552 | View.MeasureSpec.makeMeasureSpec(widthSize, widthMode), 553 | View.MeasureSpec.makeMeasureSpec(heightSize, heightMode) 554 | ) 555 | //标准方法2 556 | layout(0, 0, measuredWidth, measuredHeight) 557 | 558 | //标准方法3 559 | //draw(canvas) 560 | } 561 | } 562 | 563 | /** 564 | * 绘制分割线, 请不要使用 foreground 属性. 565 | * */ 566 | var drawOverDecoration: ( 567 | canvas: Canvas, 568 | paint: Paint, 569 | viewHolder: RecyclerView.ViewHolder, 570 | overRect: Rect 571 | ) -> Unit = 572 | { canvas, paint, viewHolder, overRect -> 573 | 574 | canvas.save() 575 | canvas.translate(overRect.left.toFloat(), overRect.top.toFloat()) 576 | 577 | viewHolder.itemView.draw(canvas) 578 | 579 | if (enableDrawShadow) { 580 | drawOverShadowDecoration.invoke(canvas, paint, viewHolder, overRect) 581 | } 582 | 583 | canvas.restore() 584 | } 585 | 586 | /**是否激活阴影绘制*/ 587 | var enableDrawShadow = true 588 | 589 | /** 590 | * 绘制分割线下面的阴影, 或者其他而外的信息 591 | * */ 592 | var drawOverShadowDecoration: ( 593 | canvas: Canvas, 594 | paint: Paint, 595 | viewHolder: RecyclerView.ViewHolder, 596 | overRect: Rect 597 | ) -> Unit = 598 | { canvas, paint, viewHolder, overRect -> 599 | 600 | if (overRect.top == 0) { 601 | //分割线完全显示的情况下, 才绘制阴影 602 | val shadowTop = overRect.bottom.toFloat() 603 | val shadowHeight = 4 * dp 604 | 605 | paint.shader = LinearGradient( 606 | 0f, shadowTop, 0f, 607 | shadowTop + shadowHeight, 608 | intArrayOf( 609 | Color.parseColor("#40000000"), 610 | Color.TRANSPARENT /*Color.parseColor("#40000000")*/ 611 | ), 612 | null, Shader.TileMode.CLAMP 613 | ) 614 | 615 | //绘制阴影 616 | canvas.drawRect( 617 | overRect.left.toFloat(), 618 | shadowTop, 619 | overRect.right.toFloat(), 620 | shadowTop + shadowHeight, 621 | paint 622 | ) 623 | } 624 | } 625 | } 626 | } 627 | 628 | fun Rect.clear() { 629 | set(0, 0, 0, 0) 630 | } 631 | 632 | public inline fun T.nowTime() = System.currentTimeMillis() 633 | 634 | 635 | /** 636 | * 获取View, 相对于手机屏幕的矩形 637 | * */ 638 | public fun View.getViewRect(result: Rect = Rect()): Rect { 639 | var offsetX = 0 640 | var offsetY = 0 641 | 642 | //横屏, 并且显示了虚拟导航栏的时候. 需要左边偏移 643 | //只计算一次 644 | (context as? Activity)?.let { 645 | it.window.decorView.getGlobalVisibleRect(result) 646 | if (result.width() > result.height()) { 647 | //横屏了 648 | offsetX = -navBarHeight(it) 649 | } 650 | } 651 | 652 | return getViewRect(offsetX, offsetY, result) 653 | } 654 | 655 | /** 656 | * 获取View, 相对于手机屏幕的矩形, 带皮阿尼一 657 | * */ 658 | public fun View.getViewRect(offsetX: Int, offsetY: Int, result: Rect = Rect()): Rect { 659 | //可见位置的坐标, 超出屏幕的距离会被剃掉 660 | //getGlobalVisibleRect(r) 661 | val r2 = IntArray(2) 662 | //val r3 = IntArray(2) 663 | //相对于屏幕的坐标 664 | getLocationOnScreen(r2) 665 | //相对于窗口的坐标 666 | //getLocationInWindow(r3) 667 | 668 | val left = r2[0] + offsetX 669 | val top = r2[1] + offsetY 670 | 671 | result.set(left, top, left + measuredWidth, top + measuredHeight) 672 | return result 673 | } 674 | 675 | 676 | /** 677 | * 导航栏的高度(如果显示了) 678 | */ 679 | fun navBarHeight(context: Context): Int { 680 | var result = 0 681 | 682 | if (context is Activity) { 683 | val decorRect = Rect() 684 | val windowRect = Rect() 685 | 686 | context.window.decorView.getGlobalVisibleRect(decorRect) 687 | context.window.findViewById(Window.ID_ANDROID_CONTENT).getGlobalVisibleRect(windowRect) 688 | 689 | if (decorRect.width() > decorRect.height()) { 690 | //横屏 691 | result = decorRect.width() - windowRect.width() 692 | } else { 693 | //竖屏 694 | result = decorRect.bottom - windowRect.bottom 695 | } 696 | } 697 | 698 | return result 699 | } 700 | 701 | public val T.dp: Float by lazy { 702 | Resources.getSystem()?.displayMetrics?.density ?: 0f 703 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration 2 | 3 | import android.os.Bundle 4 | import android.support.design.widget.Snackbar 5 | import android.support.v7.app.AppCompatActivity 6 | import android.support.v7.widget.LinearLayoutManager 7 | import android.widget.Toast 8 | import com.angcyo.hover.item.decoration.dsl.DslAdapter 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | 11 | class MainActivity : AppCompatActivity() { 12 | 13 | lateinit var baseViewHolder: RBaseViewHolder 14 | 15 | val itemDecoration = HoverItemDecoration() 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main) 20 | setSupportActionBar(toolbar) 21 | 22 | baseViewHolder = RBaseViewHolder(window.decorView) 23 | 24 | fab.setOnClickListener { view -> 25 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 26 | .setAction("Action", null).show() 27 | } 28 | 29 | 30 | gridLayoutTest() 31 | 32 | baseViewHolder.click(R.id.grid_button) { 33 | gridLayoutTest() 34 | } 35 | 36 | baseViewHolder.click(R.id.linear_button) { 37 | linearLayoutTest() 38 | } 39 | 40 | baseViewHolder.click(R.id.d1) { 41 | itemDecoration.let { 42 | it.detachedFromRecyclerView() 43 | it.attachToRecyclerView(baseViewHolder.rv(R.id.recycler_view)) { 44 | enableTouchEvent = false 45 | enableDrawableState = false 46 | } 47 | } 48 | } 49 | baseViewHolder.click(R.id.d2) { 50 | itemDecoration.let { 51 | it.detachedFromRecyclerView() 52 | it.attachToRecyclerView(baseViewHolder.rv(R.id.recycler_view)) { 53 | enableTouchEvent = true 54 | enableDrawableState = false 55 | } 56 | } 57 | } 58 | baseViewHolder.click(R.id.d3) { 59 | itemDecoration.let { 60 | it.detachedFromRecyclerView() 61 | it.attachToRecyclerView(baseViewHolder.rv(R.id.recycler_view)) { 62 | enableTouchEvent = true 63 | enableDrawableState = true 64 | } 65 | } 66 | } 67 | 68 | baseViewHolder.rv(R.id.recycler_view)?.apply { 69 | itemDecoration.let { 70 | it.detachedFromRecyclerView() 71 | it.attachToRecyclerView(this) 72 | } 73 | } 74 | } 75 | 76 | fun gridLayoutTest() { 77 | baseViewHolder.rv(R.id.recycler_view)?.apply { 78 | 79 | dslAdapter(4) { 80 | for (i in 0..2) { 81 | renderImageItem(this, true) 82 | } 83 | 84 | for (i in 0..5) { 85 | renderTextItem(this, 4) 86 | for (i in 0..5) { 87 | renderImageItem(this, true) 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | fun linearLayoutTest() { 95 | baseViewHolder.rv(R.id.recycler_view)?.apply { 96 | layoutManager = LinearLayoutManager(applicationContext) 97 | 98 | dslAdapter { 99 | for (i in 0..2) { 100 | renderImageItem(this) 101 | } 102 | 103 | for (i in 0..5) { 104 | renderTextItem(this) 105 | 106 | for (i in 0..1) { 107 | renderImageItem(this) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | fun renderImageItem(dslAdapter: DslAdapter, grid: Boolean = false) { 115 | dslAdapter.renderItem { 116 | itemLayoutId = if (grid) R.layout.item_image_little else R.layout.item_image 117 | 118 | itemBind = { itemHolder, position, _ -> 119 | itemHolder.clickItem { 120 | show("戳到人家[鼻孔]啦:$position") 121 | } 122 | } 123 | } 124 | } 125 | 126 | fun renderTextItem(dslAdapter: DslAdapter, spanCount: Int = 1) { 127 | dslAdapter.renderItem { 128 | /**极致体验, 想哪悬停, 就哪悬停*/ 129 | itemIsHover = true 130 | itemSpanCount = spanCount 131 | 132 | itemLayoutId = R.layout.item_text 133 | 134 | itemBind = { itemHolder, itemPosition, _ -> 135 | itemHolder.tv(R.id.text_view)?.text = "位置$itemPosition" 136 | 137 | itemHolder.clickItem { 138 | show("点击位置:$itemPosition") 139 | } 140 | 141 | itemHolder.click(R.id.check_box) { 142 | show("CheckBox:$itemPosition") 143 | } 144 | } 145 | } 146 | } 147 | 148 | fun show(text: CharSequence) { 149 | Toast.makeText(applicationContext, text, Toast.LENGTH_SHORT).show() 150 | 151 | title = "${nowTime()} -> $text" 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/RBaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import android.widget.TextView 6 | 7 | /** 8 | * 9 | * Email:angcyo@126.com 10 | * @author angcyo 11 | * @date 2019/05/09 12 | * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. 13 | */ 14 | 15 | class RBaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 16 | 17 | fun v(id: Int): T? = itemView.findViewById(id) 18 | 19 | fun rv(id: Int): RecyclerView? = itemView.findViewById(id) 20 | 21 | fun tv(id: Int): TextView? = itemView.findViewById(id) 22 | 23 | fun click(id: Int, listener: (View) -> Unit) { 24 | v(id)?.setOnClickListener { 25 | listener.invoke(it) 26 | } 27 | } 28 | 29 | fun clickItem(listener: (View) -> Unit) { 30 | itemView.setOnClickListener { 31 | listener.invoke(it) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/RecyclerEx.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration 2 | 3 | import android.support.v7.widget.GridLayoutManager 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.support.v7.widget.RecyclerView 6 | import com.angcyo.hover.item.decoration.dsl.DslAdapter 7 | import com.angcyo.hover.item.decoration.dsl.DslAdapterItem 8 | import com.angcyo.hover.item.decoration.dsl.DslRecyclerScroll 9 | 10 | /** 11 | * 12 | * Email:angcyo@126.com 13 | * @author angcyo 14 | * @date 2019/05/07 15 | * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. 16 | */ 17 | 18 | public fun RecyclerView.dslAdapter(init: DslAdapter.() -> Unit) { 19 | val dslAdapter = DslAdapter(context) 20 | dslAdapter.init() 21 | adapter = dslAdapter 22 | } 23 | 24 | public fun RecyclerView.dslAdapter(spanCount: Int = 1, init: DslAdapter.() -> Unit) { 25 | val dslAdapter = DslAdapter(context) 26 | dslAdapter.init() 27 | 28 | layoutManager = GridLayoutManager(context, spanCount).apply { 29 | spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 30 | override fun getSpanSize(position: Int): Int { 31 | return dslAdapter.getItemData(position).itemSpanCount 32 | } 33 | } 34 | } 35 | adapter = dslAdapter 36 | } 37 | 38 | public fun DslAdapter.renderItem(init: DslAdapterItem.() -> Unit) { 39 | val adapterItem = DslAdapterItem() 40 | adapterItem.init() 41 | addLastItem(adapterItem) 42 | } 43 | 44 | public fun RecyclerView.onScroll(init: DslRecyclerScroll.() -> Unit) { 45 | val dslRecyclerView = DslRecyclerScroll() 46 | dslRecyclerView.init() 47 | addOnScrollListener(object : RecyclerView.OnScrollListener() { 48 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 49 | super.onScrolled(recyclerView, dx, dy) 50 | 51 | val layoutManager = recyclerView.layoutManager 52 | if (layoutManager is LinearLayoutManager) { 53 | dslRecyclerView.firstItemAdapterPosition = layoutManager.findFirstVisibleItemPosition() 54 | dslRecyclerView.firstItemCompletelyVisibleAdapterPosition = 55 | layoutManager.findFirstCompletelyVisibleItemPosition() 56 | } else { 57 | 58 | } 59 | 60 | dslRecyclerView.onRecyclerScrolled.invoke(recyclerView, dx, dy) 61 | } 62 | 63 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 64 | super.onScrollStateChanged(recyclerView, newState) 65 | dslRecyclerView.onRecyclerScrollStateChanged.invoke(recyclerView, newState) 66 | } 67 | }) 68 | } 69 | 70 | public fun RecyclerView.clearItemDecoration() { 71 | for (i in itemDecorationCount - 1 downTo 0) { 72 | removeItemDecorationAt(i) 73 | } 74 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/dsl/DslAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration.dsl 2 | 3 | import android.content.Context 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.angcyo.hover.item.decoration.RBaseViewHolder 9 | 10 | /** 11 | * 12 | * Email:angcyo@126.com 13 | * @author angcyo 14 | * @date 2019/05/07 15 | * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. 16 | */ 17 | open class DslAdapter(var context: Context) : RecyclerView.Adapter() { 18 | 19 | val mAllDatas = mutableListOf() 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RBaseViewHolder { 22 | var itemView: View? = null 23 | var viewHolder: RBaseViewHolder? = null 24 | var itemLayoutId = -1 25 | if (itemView == null) { 26 | itemLayoutId = getItemLayoutId(viewType) 27 | try { 28 | itemView = LayoutInflater.from(context).inflate(itemLayoutId, parent, false) 29 | } catch (e: Exception) { 30 | e.printStackTrace() 31 | } 32 | 33 | } 34 | viewHolder = RBaseViewHolder(itemView!!) 35 | return viewHolder 36 | } 37 | 38 | override fun onBindViewHolder(p0: RBaseViewHolder, p1: Int) { 39 | onBindView(p0, p1, getItemData(p1)) 40 | } 41 | 42 | override fun getItemViewType(position: Int): Int { 43 | return getItemType(position) 44 | } 45 | 46 | /** 47 | * 在最后的位置插入数据 48 | */ 49 | fun addLastItem(bean: DslAdapterItem) { 50 | val startPosition = mAllDatas.size 51 | mAllDatas.add(bean) 52 | notifyItemInserted(startPosition) 53 | notifyItemRangeChanged(startPosition, itemCount) 54 | } 55 | 56 | fun getItemData(position: Int): DslAdapterItem { 57 | return mAllDatas[position] 58 | } 59 | 60 | override fun getItemCount(): Int { 61 | return mAllDatas.size 62 | } 63 | 64 | fun getItemLayoutId(viewType: Int): Int { 65 | return viewType 66 | } 67 | 68 | fun getItemType(position: Int): Int { 69 | return getItemData(position).itemLayoutId 70 | } 71 | 72 | fun onBindView(holder: RBaseViewHolder, position: Int, bean: DslAdapterItem?) { 73 | bean?.let { 74 | it.itemBind.invoke(holder, position, it) 75 | } 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/dsl/DslAdapterItem.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration.dsl 2 | 3 | import com.angcyo.hover.item.decoration.RBaseViewHolder 4 | 5 | /** 6 | * 7 | * Email:angcyo@126.com 8 | * @author angcyo 9 | * @date 2019/05/07 10 | * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. 11 | */ 12 | open class DslAdapterItem { 13 | 14 | /** 15 | * 在 GridLayoutManager 中, 需要占多少个 span 16 | * */ 17 | var itemSpanCount = 1 18 | 19 | /**布局的xml id, 必须设置.*/ 20 | var itemLayoutId: Int = -1 21 | 22 | /**附加的数据*/ 23 | var itemData: Any? = null 24 | 25 | /** 26 | * 是否需要悬停, 极致体验, 需要悬停就设置为 true, 否则 false 27 | * */ 28 | var itemIsHover = false 29 | 30 | /**界面绑定*/ 31 | var itemBind: (itemHolder: RBaseViewHolder, itemPosition: Int, adapterItem: DslAdapterItem) -> Unit = { _, _, _ -> } 32 | } 33 | -------------------------------------------------------------------------------- /demo/src/main/java/com/angcyo/hover/item/decoration/dsl/DslRecyclerScroll.kt: -------------------------------------------------------------------------------- 1 | package com.angcyo.hover.item.decoration.dsl 2 | 3 | import android.support.v7.widget.RecyclerView 4 | 5 | /** 6 | * 7 | * Email:angcyo@126.com 8 | * @author angcyo 9 | * @date 2019/05/08 10 | * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. 11 | */ 12 | class DslRecyclerScroll { 13 | 14 | var firstItemAdapterPosition = RecyclerView.NO_POSITION 15 | 16 | var firstItemCompletelyVisibleAdapterPosition = RecyclerView.NO_POSITION 17 | 18 | /** 19 | * @see RecyclerView.OnScrollListener.onScrolled 20 | * */ 21 | var onRecyclerScrolled: (recyclerView: RecyclerView, dx: Int, dy: Int) -> Unit = { _, _, _ -> } 22 | 23 | /** 24 | * @see RecyclerView.OnScrollListener.onScrollStateChanged 25 | * */ 26 | var onRecyclerScrollStateChanged: (recyclerView: RecyclerView, newState: Int) -> Unit = { _, _ -> } 27 | 28 | } -------------------------------------------------------------------------------- /demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable-xhdpi/peppq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angcyo/HoverItemDecoration/5f56aa968dd784db0e76d4ce3888497b172c3a14/demo/src/main/res/drawable-xhdpi/peppq.jpg -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/main_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/main_selector_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/transparent_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/transparent_selector_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 24 | 25 |