diff --git a/demo/build.gradle b/demo/build.gradle index dae1fe3..d1691b2 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -3,12 +3,12 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' android { - compileSdk 33 + compileSdk Integer.parseInt(C_SDK) defaultConfig { applicationId "com.angcyo.dslitem.demo" - minSdkVersion 16 - targetSdkVersion 33 + minSdk Integer.parseInt(M_SDK) + targetSdk Integer.parseInt(T_SDK) versionCode 1 versionName "1.0" @@ -27,7 +27,6 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' @@ -36,4 +35,5 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation project(":dslitem") + implementation project(":dslitem2") } diff --git a/demo/src/main/java/com/angcyo/dslitem/demo/MainActivity.kt b/demo/src/main/java/com/angcyo/dslitem/demo/MainActivity.kt index 8e222ce..366a307 100644 --- a/demo/src/main/java/com/angcyo/dslitem/demo/MainActivity.kt +++ b/demo/src/main/java/com/angcyo/dslitem/demo/MainActivity.kt @@ -4,7 +4,17 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import com.angcyo.dsladapter.initDslAdapter -import com.angcyo.item.* +import com.angcyo.item.DslBaseEditItem +import com.angcyo.item.DslBaseInfoItem +import com.angcyo.item.DslBaseLabelItem +import com.angcyo.item.DslButtonItem +import com.angcyo.item.DslGridItem +import com.angcyo.item.DslLabelEditItem +import com.angcyo.item.DslLabelTextItem +import com.angcyo.item.DslSwitchInfoItem +import com.angcyo.item.DslTextInfoItem +import com.angcyo.item.DslTextItem +import com.angcyo.item.style.itemEditText class MainActivity : AppCompatActivity() { @@ -16,8 +26,7 @@ class MainActivity : AppCompatActivity() { render { DslBaseEditItem()() { itemEditText = this::class.java.simpleName - _lastEditSelectionStart = itemEditText?.length ?: -1 - + editItemConfig._lastEditSelectionStart = itemEditText?.length ?: -1 configEditTextStyle { hint = "请输入..." } @@ -62,7 +71,7 @@ class MainActivity : AppCompatActivity() { DslLabelEditItem()() { itemLabelText = "Label" itemEditText = this::class.java.simpleName - _lastEditSelectionStart = itemEditText?.length ?: -1 + editItemConfig._lastEditSelectionStart = itemEditText?.length ?: -1 configEditTextStyle { hint = "请输入..." diff --git a/dslitem/build.gradle b/dslitem/build.gradle index 6ab523d..77eb9a6 100644 --- a/dslitem/build.gradle +++ b/dslitem/build.gradle @@ -2,13 +2,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdk 33 + compileSdk Integer.parseInt(C_SDK) defaultConfig { vectorDrawables.useSupportLibrary = true - minSdkVersion 16 - targetSdkVersion 33 + minSdk Integer.parseInt(M_SDK) + targetSdk Integer.parseInt(T_SDK) consumerProguardFiles 'consumer-rules.pro' } @@ -16,8 +16,6 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - //https://mvnrepository.com/artifact/androidx.appcompat/appcompat implementation 'androidx.appcompat:appcompat:1.6.1' @@ -28,11 +26,11 @@ dependencies { api 'androidx.recyclerview:recyclerview:1.3.0' //https://github.com/angcyo/DslAdapter - api 'com.github.angcyo:DslAdapter:5.2.0' + api 'com.github.angcyo:DslAdapter:6.0.1' //https://github.com/angcyo/DslSpan api 'com.github.angcyo:DslSpan:1.1.0' //https://github.com/angcyo/DslButton - api 'com.github.angcyo:DslButton:1.2.0' + api 'com.github.angcyo:DslButton:1.3.0' } diff --git a/dslitem/src/main/java/com/angcyo/drawable/DslAttrBadgeDrawable.kt b/dslitem/src/main/java/com/angcyo/drawable/DslAttrBadgeDrawable.kt new file mode 100644 index 0000000..bfef55c --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/drawable/DslAttrBadgeDrawable.kt @@ -0,0 +1,113 @@ +package com.angcyo.drawable + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.util.AttributeSet +import android.view.Gravity +import com.angcyo.dpi +import com.angcyo.item.R + +/** + * 角标 + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/13 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +open class DslAttrBadgeDrawable : DslBadgeDrawable() { + + /**是否要绘制角标*/ + var drawBadge: Boolean = true + + override fun initAttribute(context: Context, attributeSet: AttributeSet?) { + val typedArray = + context.obtainStyledAttributes(attributeSet, R.styleable.DslAttrBadgeDrawable) + drawBadge = typedArray.getBoolean(R.styleable.DslAttrBadgeDrawable_r_draw_badge, drawBadge) + gradientSolidColor = + typedArray.getColor( + R.styleable.DslAttrBadgeDrawable_r_badge_solid_color, + Color.RED + ) + badgeTextColor = + typedArray.getColor( + R.styleable.DslAttrBadgeDrawable_r_badge_text_color, + Color.WHITE + ) + badgeGravity = typedArray.getInt( + R.styleable.DslAttrBadgeDrawable_r_badge_gravity, + Gravity.TOP or Gravity.RIGHT + ) + badgeOffsetX = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_offset_x, + badgeOffsetX + ) + badgeCircleRadius = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_circle_radius, + 4 * dpi + ) + val badgeRadius = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_radius, + 10 * dpi + ) + cornerRadius(badgeRadius.toFloat()) + badgeOffsetY = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_offset_y, + badgeOffsetY + ) + badgePaddingLeft = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_padding_left, + 4 * dpi + ) + badgePaddingRight = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_padding_right, + 4 * dpi + ) + badgePaddingTop = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_padding_top, + 0 + ) + badgePaddingBottom = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_padding_bottom, + 0 + ) + //脚本文本内容 + badgeText = typedArray.getString(R.styleable.DslAttrBadgeDrawable_r_badge_text) + badgeTextSize = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_text_size, + 12 * dpi + ).toFloat() + + //自定义的背景 + originDrawable = + typedArray.getDrawable(R.styleable.DslAttrBadgeDrawable_r_badge_bg_drawable) + + badgeTextOffsetX = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_text_offset_x, + badgeTextOffsetX + ) + + badgeTextOffsetY = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_text_offset_y, + badgeTextOffsetY + ) + + badgeCircleOffsetX = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_circle_offset_x, + badgeTextOffsetX + ) + + badgeCircleOffsetY = typedArray.getDimensionPixelOffset( + R.styleable.DslAttrBadgeDrawable_r_badge_circle_offset_y, + badgeTextOffsetY + ) + typedArray.recycle() + super.initAttribute(context, attributeSet) + } + + override fun draw(canvas: Canvas) { + if (drawBadge) { + super.draw(canvas) + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/drawable/DslBadgeDrawable.kt b/dslitem/src/main/java/com/angcyo/drawable/DslBadgeDrawable.kt new file mode 100644 index 0000000..7ea7bc9 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/drawable/DslBadgeDrawable.kt @@ -0,0 +1,308 @@ +package com.angcyo.drawable + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.text.TextUtils +import android.util.AttributeSet +import android.view.Gravity +import com.angcyo.dp +import com.angcyo.dpi +import com.angcyo.drawable.base.DslGradientDrawable +import com.angcyo.dsladapter.textHeight +import com.angcyo.item.R +import com.angcyo.item._color +import com.angcyo.item._dimen +import com.angcyo.item.textWidth +import com.angcyo.widget.DslGravity +import com.angcyo.widget.isGravityCenter +import kotlin.math.max + +/** + * 未读数, 未读小红点, 角标绘制Drawable + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/13 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +open class DslBadgeDrawable : DslGradientDrawable() { + + val dslGravity = DslGravity() + + /**重力*/ + var badgeGravity: Int = Gravity.CENTER //Gravity.TOP or Gravity.RIGHT + + /**角标文本颜色*/ + var badgeTextColor = Color.WHITE + + /**角标的文本(默认居中绘制文本,暂不支持修改), 空字符串会绘制成小圆点 + * null 不绘制角标 + * "" 空字符绘制圆点 + * 其他 正常绘制 + * */ + var badgeText: String? = null + + /**角标的文本大小*/ + var badgeTextSize: Float = 12 * dp + set(value) { + field = value + textPaint.textSize = field + } + + /**当[badgeText]只有1个字符时, 使用圆形背景*/ + var badgeAutoCircle: Boolean = true + + /**圆点状态时的半径大小*/ + var badgeCircleRadius = 4 * dpi + + /**原点状态下, 单独配置的偏移 + * 默认等于[badgeTextOffsetX] [badgeTextOffsetY]*/ + var badgeCircleOffsetX: Int = 0 + var badgeCircleOffsetY: Int = 0 + + /**额外偏移距离, 会根据[Gravity]自动取负值*/ + var badgeOffsetX: Int = 2 * dpi + var badgeOffsetY: Int = 2 * dpi + + /**文本偏移*/ + var badgeTextOffsetX: Int = 0 + var badgeTextOffsetY: Int = 0 + + /**圆点状态时无效*/ + var badgePaddingLeft = 0 + var badgePaddingRight = 0 + var badgePaddingTop = 0 + var badgePaddingBottom = 0 + + /**最小的高度大小, px. 大于0生效; 圆点时属性无效*/ + var badgeMinHeight = -2 + + /**最小的宽度大小, px. 大于0生效; 圆点时属性无效; + * -1 表示使用使用计算出来的高度值*/ + var badgeMinWidth = -2 + + //计算属性 + val textWidth: Float + get() = textPaint.textWidth(badgeText) + + //最大的宽度 + val maxWidth: Int + get() = max( + textWidth.toInt(), + originDrawable?.minimumWidth ?: 0 + ) + badgePaddingLeft + badgePaddingRight + + //最大的高度 + val maxHeight: Int + get() = max( + textHeight.toInt(), + originDrawable?.minimumHeight ?: 0 + ) + badgePaddingTop + badgePaddingBottom + + val textHeight: Float + get() = textPaint.textHeight() + + //原型状态 + val isCircle: Boolean + get() = TextUtils.isEmpty(badgeText) + + override fun initAttribute(context: Context, attributeSet: AttributeSet?) { + super.initAttribute(context, attributeSet) + updateOriginDrawable() + } + + override fun draw(canvas: Canvas) { + //super.draw(canvas) + + if (badgeText == null) { + return + } + + with(dslGravity) { + gravity = if (isViewRtl) { + when (badgeGravity) { + Gravity.RIGHT -> Gravity.LEFT + Gravity.LEFT -> Gravity.RIGHT + else -> badgeGravity + } + } else { + badgeGravity + } + + setGravityBounds(bounds) + + if (isCircle) { + gravityOffsetX = badgeCircleOffsetX.toFloat() + gravityOffsetY = badgeCircleOffsetY.toFloat() + } else { + gravityOffsetX = badgeOffsetX.toFloat() + gravityOffsetY = badgeOffsetY.toFloat() + } + + val textWidth = textPaint.textWidth(badgeText) + val textHeight = textPaint.textHeight() + + val drawHeight = if (isCircle) { + badgeCircleRadius.toFloat() * 2 + } else { + val height = textHeight + badgePaddingTop + badgePaddingBottom + if (badgeMinHeight > 0) { + max(height, badgeMinHeight.toFloat()) + } else { + height + } + } + + val drawWidth = if (isCircle) { + badgeCircleRadius.toFloat() * 2 + } else { + val width = textWidth + badgePaddingLeft + badgePaddingRight + if (badgeMinWidth == -1) { + max(width, drawHeight) + } else if (badgeMinWidth > 0) { + max(width, badgeMinWidth.toFloat()) + } else { + width + } + } + + applyGravity(drawWidth, drawHeight) { centerX, centerY -> + + if (isCircle) { + textPaint.color = gradientSolidColor + + //圆心计算 + val cx: Float + val cy: Float + if (gravity.isGravityCenter()) { + cx = centerX.toFloat() + cy = centerY.toFloat() + } else { + cx = centerX.toFloat() + _gravityOffsetX + cy = centerY.toFloat() + _gravityOffsetY + } + + //绘制圆 + textPaint.color = gradientSolidColor + canvas.drawCircle( + cx, + cy, + badgeCircleRadius.toFloat(), + textPaint + ) + + //圆的描边 + if (gradientStrokeWidth > 0 && gradientStrokeColor != Color.TRANSPARENT) { + val oldWidth = textPaint.strokeWidth + val oldStyle = textPaint.style + + textPaint.color = gradientStrokeColor + textPaint.strokeWidth = gradientStrokeWidth.toFloat() + textPaint.style = Paint.Style.STROKE + + canvas.drawCircle( + cx, + cy, + badgeCircleRadius.toFloat(), + textPaint + ) + + textPaint.strokeWidth = oldWidth + textPaint.style = oldStyle + } + + } else { + textPaint.color = badgeTextColor + + val textDrawX: Float = centerX - textWidth / 2 + val textDrawY: Float = centerY + textHeight / 2 + + val bgLeft = _gravityLeft.toInt() + val bgTop = _gravityTop.toInt() + + //绘制背景 + if (badgeAutoCircle && badgeText?.length == 1) { + if (gradientSolidColor != Color.TRANSPARENT) { + textPaint.color = gradientSolidColor + canvas.drawCircle( + centerX, + centerY, + max(maxWidth, maxHeight).toFloat() / 2, + textPaint + ) + } + } else { + originDrawable?.apply { + setBounds( + bgLeft, bgTop, + (bgLeft + drawWidth).toInt(), + (bgTop + drawHeight).toInt() + ) + draw(canvas) + } + } + + //绘制文本 + textPaint.color = badgeTextColor + canvas.drawText( + badgeText!!, + textDrawX + badgeTextOffsetX, + textDrawY - textPaint.descent() + badgeTextOffsetY, + textPaint + ) + } + } + } + } + + override fun getIntrinsicWidth(): Int { + val width = if (isCircle) { + badgeCircleRadius * 2 + badgeCircleOffsetX + } else if (badgeAutoCircle && badgeText?.length == 1) { + max(maxWidth, maxHeight) + badgeCircleOffsetX + } else { + maxWidth + badgeOffsetX + } + return max(badgeMinWidth, width) + } + + override fun getIntrinsicHeight(): Int { + val height = if (isCircle) { + badgeCircleRadius * 2 + badgeCircleOffsetY + } else if (badgeAutoCircle && badgeText?.length == 1) { + max(maxWidth, maxHeight) + badgeCircleOffsetY + } else { + maxHeight + } + return max(badgeMinHeight, height) + } +} + +/**未读数提示角标 + * [number] null 不绘制角标 + * "" 空字符绘制圆点 + * 其他 正常绘制 + * */ +fun dslNumberBadgeDrawable( + number: String?, + textColor: Int = Color.WHITE, + solidColor: Int = _color(R.color.colorAccent), + radius: Int = _dimen(R.dimen.lib_radius_common), + dsl: DslBadgeDrawable.() -> Unit = {} +): DslBadgeDrawable { + return DslBadgeDrawable().apply { + badgeText = number + badgeTextColor = textColor + + badgePaddingLeft = _dimen(R.dimen.lib_dpi) + badgePaddingRight = _dimen(R.dimen.lib_dpi) + + configDrawable { + fillRadii(radius) + gradientSolidColor = solidColor + } + + dsl() + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/extend/IToText.kt b/dslitem/src/main/java/com/angcyo/extend/IToText.kt new file mode 100644 index 0000000..8470157 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/extend/IToText.kt @@ -0,0 +1,12 @@ +package com.angcyo.extend + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/07/20 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IToText { + fun toText(): CharSequence? +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/extend/IToValue.kt b/dslitem/src/main/java/com/angcyo/extend/IToValue.kt new file mode 100644 index 0000000..bb4df9e --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/extend/IToValue.kt @@ -0,0 +1,12 @@ +package com.angcyo.extend + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/07/20 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IToValue { + fun toValue(): Any? +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/AnimEx.kt b/dslitem/src/main/java/com/angcyo/item/AnimEx.kt new file mode 100644 index 0000000..0a85e2a --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/AnimEx.kt @@ -0,0 +1,714 @@ +package com.angcyo.item + +import android.animation.* +import android.annotation.TargetApi +import android.content.Context +import android.graphics.Camera +import android.graphics.Matrix +import android.graphics.Rect +import android.os.Build +import android.util.Property +import android.view.View +import android.view.ViewAnimationUtils +import android.view.animation.* +import androidx.annotation.AnimRes +import androidx.annotation.AnimatorRes +import androidx.annotation.RequiresApi +import androidx.core.animation.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.doOnPreDraw +import com.angcyo.item.Anim.ANIM_DURATION +import com.angcyo.library.animation.YRotateAnimation +import com.angcyo.library.app +import com.angcyo.library.component.MatrixEvaluator +import com.angcyo.library.component.RAnimationListener +import java.lang.ref.WeakReference + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/20 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ + +object Anim { + /**动画默认时长*/ + var ANIM_DURATION = 300L +} + +/**从指定资源id中, 加载动画[Animation]*/ +fun animationOf(context: Context = app(), @AnimRes id: Int): Animation? { + try { + if (id == 0 || id == -1) { + return null + } + return AnimationUtils.loadAnimation(context, id) + } catch (e: Exception) { + //e.printStackTrace() + L.w(e.message) + return null + } +} + +/**从指定资源id中, 加载动画[Animator]*/ +fun animatorOf(context: Context = app(), @AnimatorRes id: Int): Animator? { + try { + if (id == 0 || id == -1) { + return null + } + return AnimatorInflater.loadAnimator(context, id) + } catch (e: Exception) { + //e.printStackTrace() + L.w(e.message) + return null + } +} + +fun anim(from: Int, to: Int, config: AnimatorConfig.() -> Unit = {}): ValueAnimator { + return _animator(ValueAnimator.ofInt(from, to), config) +} + +fun anim(from: Float, to: Float, config: AnimatorConfig.() -> Unit = {}): ValueAnimator { + return _animator(ValueAnimator.ofFloat(from, to), config) +} + +fun _animator(animator: ValueAnimator, config: AnimatorConfig.() -> Unit = {}): ValueAnimator { + val animatorConfig = AnimatorConfig() + + animator.duration = ANIM_DURATION + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { + animatorConfig.onAnimatorUpdateValue(it.animatedValue, it.animatedFraction) + } + animator.addListener(RAnimatorListener().apply { + onAnimatorFinish = { _, _ -> + animatorConfig.onAnimatorEnd(animator) + } + }) + + animatorConfig.config() + animatorConfig.onAnimatorConfig(animator) + + animator.start() + return animator +} + +class AnimatorConfig { + + /**动画时长*/ + var animatorDuration = ANIM_DURATION + + /**配置动画, 比如时长, 差值器等*/ + var onAnimatorConfig: (animator: ValueAnimator) -> Unit = { + it.duration = animatorDuration + } + + /**动画值改变的监听*/ + var onAnimatorUpdateValue: (value: Any, fraction: Float) -> Unit = { _, _ -> } + + /**动画结束的监听*/ + var onAnimatorEnd: (animator: ValueAnimator) -> Unit = {} +} + +/**缩放属性动画*/ +fun View.scale( + from: Float, + to: Float, + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + onEnd: () -> Unit = {} +): ValueAnimator { + return anim(from, to) { + onAnimatorUpdateValue = { value, _ -> + scaleX = value as Float + scaleY = scaleX + } + + onAnimatorConfig = { + it.duration = duration + it.interpolator = interpolator + onAnimatorEnd = { _ -> onEnd() } + } + } +} + +/**平移属性动画*/ +fun View.translationX( + from: Float, + to: Float, + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + onEnd: () -> Unit = {} +): ValueAnimator { + return anim(from, to) { + onAnimatorUpdateValue = { value, _ -> + translationX = value as Float + } + + onAnimatorConfig = { + it.duration = duration + it.interpolator = interpolator + onAnimatorEnd = { _ -> onEnd() } + } + } +} + +fun View.translationY( + from: Float, + to: Float, + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + onEnd: () -> Unit = {} +): ValueAnimator { + return anim(from, to) { + onAnimatorUpdateValue = { value, _ -> + translationY = value as Float + } + + onAnimatorConfig = { + it.duration = duration + it.interpolator = interpolator + onAnimatorEnd = { _ -> onEnd() } + } + } +} + +/**补间动画*/ +fun View.rotateAnimation( + fromDegrees: Float = 0f, + toDegrees: Float = 360f, + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + config: RotateAnimation.() -> Unit = {}, + onEnd: (animation: Animation) -> Unit = {} +): RotateAnimation { + return RotateAnimation( + fromDegrees, + toDegrees, + RotateAnimation.RELATIVE_TO_SELF, + 0.5f, + RotateAnimation.RELATIVE_TO_SELF, + 0.5f + ).apply { + this.duration = duration + this.interpolator = interpolator + setAnimationListener(object : RAnimationListener() { + override fun onAnimationEnd(animation: Animation) { + onEnd(animation) + } + }) + config() + this@rotateAnimation.startAnimation(this) + } +} + +/**动画结束的回调*/ +fun Animation.onAnimationEnd(onEnd: (animation: Animation) -> Unit = {}) { + setAnimationListener(object : RAnimationListener() { + override fun onAnimationEnd(animation: Animation) { + onEnd(animation) + } + }) +} + +/** + * 揭露动画 + * https://developer.android.com/training/animation/reveal-or-hide-view#Reveal + * */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +fun View.reveal(action: RevealConfig.() -> Unit = {}) { + this.doOnPreDraw { + val config = RevealConfig() + if (config.centerX == 0) { + config.centerX = this.measuredWidth / 2 + } + if (config.centerY == 0) { + config.centerY = this.measuredHeight / 2 + } + if (config.endRadius == 0f) { + config.endRadius = c(config.centerX.toDouble(), config.centerY.toDouble()).toFloat() + } + + //第一次获取基础数据 + config.action() + + ViewAnimationUtils.createCircularReveal( + this, + config.centerX, + config.centerY, + config.startRadius, + config.endRadius + ).apply { + duration = config.duration + + config.animator = this + //第二次获取动画数据 + config.action() + start() + } + } +} + +data class RevealConfig( + var animator: Animator? = null, + + //如果为0, 则默认是视图的中心 + var centerX: Int = 0, + var centerY: Int = 0, + + //动画开始的半径 + var startRadius: Float = 0f, + //如果为0, 默认是视图的对角半径 + var endRadius: Float = 0f, + + //动画时长 + var duration: Long = ANIM_DURATION +) + +/**颜色渐变动画*/ +fun colorAnimator( + fromColor: Int, + toColor: Int, + infinite: Boolean = false, + interpolator: Interpolator = LinearInterpolator(), + duration: Long = ANIM_DURATION, + onEnd: (cancel: Boolean) -> Unit = {}, + config: ValueAnimator.() -> Unit = {}, + onUpdate: (animator: ValueAnimator, color: Int) -> Unit +): ValueAnimator { + //颜色动画 + val colorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor) + colorAnimator.addUpdateListener { animation -> + val color = animation.animatedValue as Int + onUpdate(animation, color) + } + colorAnimator.addListener(object : RAnimatorListener() { + override fun _onAnimatorFinish(animator: Animator, fromCancel: Boolean) { + super._onAnimatorFinish(animator, fromCancel) + onEnd(fromCancel) + } + }) + colorAnimator.interpolator = interpolator + colorAnimator.duration = duration + if (infinite) { + colorAnimator.repeatCount = ValueAnimator.INFINITE + colorAnimator.repeatMode = ValueAnimator.REVERSE + } + colorAnimator.config() + colorAnimator.start() + return colorAnimator +} + +/**一组颜色变化的动画*/ +fun colorListAnimator( + colorList: List, + infinite: Boolean = false, + interpolator: Interpolator = LinearInterpolator(), + duration: Long = colorList.size() * 1000L, + onEnd: (cancel: Boolean) -> Unit = {}, + config: ValueAnimator.() -> Unit = {}, + onUpdate: (animator: ValueAnimator, color: Int) -> Unit +): ValueAnimator { + //是否需要反序 + var reverse = false + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.addUpdateListener { animation -> + val section = colorList.size() + if (section <= 1) { + onUpdate(animation, colorList[0]) + } else { + //每一段能运行的时间 + val sectionTime = duration / (section - 1) + //当前在那一段 + val time = animation.currentPlayTime * 1f % duration //取模调整时间 + val currentStep: Int = (time / sectionTime).floor().toInt() + + //获取需要变化的颜色 + val startColor: Int + val endColor: Int + if (reverse) { + val startIndex = section - currentStep - 1 + endColor = colorList[startIndex] + startColor = colorList.getOrNull(startIndex - 1) ?: colorList.first() + } else { + startColor = colorList[currentStep] + endColor = colorList.getOrNull(currentStep + 1) ?: colorList.last() + } + + //当前的进度 + val animatedValue = animation.animatedValue as Float + val sectionProgress = 1f / (section - 1) + val currentProgress: Float = + interpolator.getInterpolation(animatedValue % sectionProgress / sectionProgress) + + onUpdate(animation, evaluateColor(currentProgress, startColor, endColor)) + } + } + animator.addListener(object : RAnimatorListener() { + + override fun onAnimationRepeat(animation: Animator) { + super.onAnimationRepeat(animation) + if (animator.repeatMode == ValueAnimator.REVERSE) { + reverse = !reverse + } + } + + override fun _onAnimatorFinish(animator: Animator, fromCancel: Boolean) { + super._onAnimatorFinish(animator, fromCancel) + onEnd(fromCancel) + } + }) + animator.interpolator = LinearInterpolator() + animator.duration = duration + if (infinite) { + animator.repeatCount = ValueAnimator.INFINITE + animator.repeatMode = ValueAnimator.REVERSE + } + animator.config() + animator.start() + return animator +} + +/**背景变化动画*/ +fun View.bgColorAnimator( + fromColor: Int, + toColor: Int, + infinite: Boolean = false, + interpolator: Interpolator = LinearInterpolator(), + duration: Long = ANIM_DURATION, + onEnd: (cancel: Boolean) -> Unit = {}, + config: ValueAnimator.() -> Unit = {} +): ValueAnimator { + //背景动画 + return colorAnimator( + fromColor, + toColor, + infinite, + interpolator, + duration, + onEnd, + config + ) { _, color -> + setBackgroundColor(color) + } +} + +/** + * 抖动 放大缩小 + */ +fun View.scaleAnimator( + from: Float = 0.5f, + to: Float = 1f, + interpolator: Interpolator = BounceInterpolator(), + onEnd: () -> Unit = {} +) { + scaleAnimator(from, from, to, to, interpolator, onEnd) +} + +fun View.scaleAnimator( + fromX: Float = 0.5f, + fromY: Float = 0.5f, + toX: Float = 1f, + toY: Float = 1f, + interpolator: Interpolator = BounceInterpolator(), + onEnd: () -> Unit = {} +) { + scaleX = fromX + scaleY = fromY + animate() + .scaleX(toX) + .scaleY(toY) + .setInterpolator(interpolator) + .setDuration(ANIM_DURATION) + .withEndAction { onEnd() } + .start() +} + +/**[Matrix]改变动画*/ +fun matrixAnimator( + startMatrix: Matrix, + endMatrix: Matrix, + duration: Long = ANIM_DURATION, + interpolator: Interpolator? = DecelerateInterpolator(), + finish: (isCancel: Boolean) -> Unit = {}, + block: (Matrix) -> Unit +): ValueAnimator { + return ObjectAnimator.ofObject(MatrixEvaluator(), startMatrix, endMatrix).apply { + this.duration = duration + this.interpolator = interpolator + this.addUpdateListener { + block(it.animatedValue as Matrix) + } + this.addListener(onEnd = { finish(false) }, onCancel = { finish(true) }) + start() + } +} + +fun matrixAnimatorFraction( + startMatrix: Matrix, + endMatrix: Matrix, + duration: Long = ANIM_DURATION, + interpolator: Interpolator? = DecelerateInterpolator(), + block: (matrix: Matrix, fraction: Float) -> Unit +): ValueAnimator { + return ObjectAnimator.ofObject(MatrixEvaluator(), startMatrix, endMatrix).apply { + this.duration = duration + this.interpolator = interpolator + addUpdateListener { + block(it.animatedValue as Matrix, it.animatedFraction) + } + start() + } +} + +/**[Rect]动画*/ +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +fun rectAnimator( + startRect: Rect, + endRect: Rect, + duration: Long = ANIM_DURATION, + interpolator: Interpolator? = DecelerateInterpolator(), + block: (Rect) -> Unit +): ValueAnimator { + return ObjectAnimator.ofObject(RectEvaluator(), startRect, endRect).apply { + this.duration = duration + this.interpolator = interpolator + addUpdateListener { + block(it.animatedValue as Rect) + } + start() + } +} + +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +fun rectAnimatorFraction( + startRect: Rect, + endRect: Rect, + duration: Long = ANIM_DURATION, + interpolator: Interpolator? = DecelerateInterpolator(), + block: (rect: Rect, fraction: Float) -> Unit +): ValueAnimator { + return ObjectAnimator.ofObject(RectEvaluator(), startRect, endRect).apply { + this.duration = duration + this.interpolator = interpolator + addUpdateListener { + block(it.animatedValue as Rect, it.animatedFraction) + } + start() + } +} + +/**clip动画, 从左到右展开显示 + * [com.angcyo.library.ex.AnimEx.clipBoundsAnimator] + * */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +fun View.clipBoundsAnimatorFromLeft( + start: Rect = Rect(0, 0, 0, mH()), + end: Rect = Rect(0, 0, mW(), mH()), + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + onEndAction: () -> Unit = {} +): ObjectAnimator = clipBoundsAnimator(start, end, duration, interpolator, onEndAction) + +/**clip动画, 从右到左隐藏 + * [com.angcyo.library.ex.AnimEx.clipBoundsAnimator] + * */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +fun View.clipBoundsAnimatorFromRightHide( + start: Rect = Rect(0, 0, mW(), mH()), + end: Rect = Rect(0, 0, 0, mH()), + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + onEndAction: () -> Unit = {} +): ObjectAnimator = clipBoundsAnimator(start, end, duration, interpolator, onEndAction) + +/**clip动画 + * [androidx.transition.ChangeClipBounds] + * */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +fun View.clipBoundsAnimator( + start: Rect = Rect(mW() / 2, mH() / 2, mW() / 2, mH() / 2), + end: Rect = Rect(0, 0, mW(), mH()), + duration: Long = ANIM_DURATION, + interpolator: Interpolator = LinearInterpolator(), + onEndAction: () -> Unit = {} +): ObjectAnimator { + ViewCompat.setClipBounds(this, start) + val evaluator = RectEvaluator(Rect()) + val animator: ObjectAnimator = ObjectAnimator.ofObject( + this, object : Property(Rect::class.java, "clipBounds") { + override fun get(view: View?): Rect? { + return ViewCompat.getClipBounds(view!!) + } + + override fun set(view: View?, clipBounds: Rect?) { + ViewCompat.setClipBounds(view!!, clipBounds) + } + }, + evaluator, start, end + ) + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + ViewCompat.setClipBounds(this@clipBoundsAnimator, null) + if (end.width() <= 0 || end.height() <= 0) { + visibility = View.GONE + } + onEndAction() + } + }) + animator.duration = duration + animator.interpolator = interpolator + animator.start() + setAnimator(animator) + return animator +} + +/**清空属性动画的相关属性*/ +fun View.clearAnimatorProperty( + scale: Boolean = true, + translation: Boolean = true, + alpha: Boolean = true +) { + if (scale) { + scaleX = 1f + scaleY = 1f + } + + if (translation) { + translationX = 0f + translationY = 0f + } + + if (alpha) { + this.alpha = 1f + } +} + +fun View.setAnimator(animator: Animator) { + setTag(R.id.lib_tag_animator, WeakReference(animator)) +} + +/**取消动画[Animator] [Animation] [Animate]*/ +fun View.cancelAnimator() { + val tag = getTag(R.id.lib_tag_animator) + var animator: Animator? = null + if (tag is WeakReference<*>) { + val any = tag.get() + if (any is Animator) { + animator = any + } + } else if (tag is Animator) { + animator = tag + } + animator?.cancel() + + //animation + clearAnimation() + //animate + animate().cancel() +} + +/**[Camera]*/ +fun rotateCameraAnimator( + from: Float = 0f, + to: Float = 180f, + config: AnimatorConfig.() -> Unit = {}, + action: Camera.(value: Float) -> Unit +): ValueAnimator { + val animatorConfig = AnimatorConfig() + animatorConfig.config() + return anim(from, to) { + onAnimatorConfig = { + it.repeatCount = ValueAnimator.INFINITE + it.repeatMode = ValueAnimator.REVERSE + + animatorConfig.onAnimatorConfig(it) + } + + onAnimatorUpdateValue = { value, fraction -> + animatorConfig.onAnimatorUpdateValue(value, fraction) + val camera = Camera() + action(camera, value as Float) + } + + onAnimatorEnd = { + animatorConfig.onAnimatorEnd(it) + } + } +} + +/** + * x轴旋转角度的动画 + * [from] 从多少角度 + * [to] 到多少角度 + * */ +fun View.rotateXAnimator( + from: Float = 0f, + to: Float = 180f, + config: AnimatorConfig.() -> Unit = { + animatorDuration = 1_000 + } +): ValueAnimator { + return rotateCameraAnimator(from, to, config) { value -> + rotateX(value) + val matrix = Matrix() + getMatrix(matrix) + val centerX = mW() / 2f + val centerY = mH() / 2f + matrix.preTranslate(-centerX, -centerY) + matrix.postTranslate(centerX, centerY) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + animationMatrix = matrix + } + } +} + +/**Y轴旋转动画*/ +fun View.rotateYAnimator( + from: Float = 0f, + to: Float = 180f, + config: AnimatorConfig.() -> Unit = { + animatorDuration = 1_000 + } +): ValueAnimator { + return rotateCameraAnimator(from, to, config) { value -> + rotateY(value) + val matrix = Matrix() + getMatrix(matrix) + val centerX = mW() / 2f + val centerY = mH() / 2f + matrix.preTranslate(-centerX, -centerY) + matrix.postTranslate(centerX, centerY) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + animationMatrix = matrix + } + } +} + +/**Y轴旋转动画*/ +fun View.rotateYAnimation( + from: Float = 0f, + to: Float = 180f, + config: YRotateAnimation.() -> Unit = { + duration = 1_000 + } +): Animation { + val animation = YRotateAnimation() + animation.from = from + animation.to = to + animation.repeatCount = ValueAnimator.INFINITE + animation.repeatMode = ValueAnimator.REVERSE + + animation.config() + + startAnimation(animation) + return animation +} + +/**无限循环*/ +fun Animation.infinite(mode: Int = ValueAnimator.RESTART) { + //repeatMode = ValueAnimator.REVERSE + repeatMode = mode + repeatCount = ValueAnimator.INFINITE +} diff --git a/dslitem/src/main/java/com/angcyo/item/DslBaseEditItem.kt b/dslitem/src/main/java/com/angcyo/item/DslBaseEditItem.kt index 9e5bfec..63c42b2 100644 --- a/dslitem/src/main/java/com/angcyo/item/DslBaseEditItem.kt +++ b/dslitem/src/main/java/com/angcyo/item/DslBaseEditItem.kt @@ -1,11 +1,9 @@ package com.angcyo.item -import android.text.InputFilter -import android.text.InputType -import android.view.Gravity -import android.widget.TextView import com.angcyo.dsladapter.DslAdapterItem import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.item.style.EditItemConfig +import com.angcyo.item.style.IEditItem /** * 输入框item基类 @@ -15,30 +13,17 @@ import com.angcyo.dsladapter.DslViewHolder * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. */ -open class DslBaseEditItem : DslBaseLabelItem() { +open class DslBaseEditItem : DslBaseLabelItem(), IEditItem { companion object { /**允许默认输入的字符长度*/ var DEFAULT_MAX_INPUT_LENGTH = 30 - } - - var itemEditText: CharSequence? = null - set(value) { - field = value - itemEditTextStyle.text = value - } - - /**统一样式配置*/ - var itemEditTextStyle = EditStyleConfig() - /**文本改变*/ - var itemTextChange: (CharSequence) -> Unit = { - onItemTextChange(it) + /**输入框文本改变节流时长, 毫秒*/ + var DEFAULT_INPUT_SHAKE_DELAY = 300L } - //用于恢复光标的位置 - var _lastEditSelectionStart = -1 - var _lastEditSelectionEnd = -1 + override var editItemConfig: EditItemConfig = EditItemConfig() init { itemLayoutId = R.layout.dsl_edit_item @@ -50,76 +35,20 @@ open class DslBaseEditItem : DslBaseLabelItem() { adapterItem: DslAdapterItem ) { super.onItemBind(itemHolder, itemPosition, adapterItem) - - itemHolder.ev(R.id.lib_edit_view)?.apply { - itemEditTextStyle.updateStyle(this) - - //放在最后监听, 防止首次setInputText, 就触发事件. - onTextChange { - _lastEditSelectionStart = selectionStart - _lastEditSelectionEnd = selectionEnd - - itemEditText = it - itemChanging = true - itemTextChange(it) - } - - restoreSelection(_lastEditSelectionStart, _lastEditSelectionEnd) - } } - open fun onItemTextChange(text: CharSequence) { - + override fun onItemViewDetachedToWindow(itemHolder: DslViewHolder, itemPosition: Int) { + super.onItemViewDetachedToWindow(itemHolder, itemPosition) + //itemHolder.ev(R.id.lib_edit_view)?.clearListeners() } - open fun configEditTextStyle(action: EditStyleConfig.() -> Unit) { - itemEditTextStyle.action() + override fun onItemViewRecycled(itemHolder: DslViewHolder, itemPosition: Int) { + super.onItemViewRecycled(itemHolder, itemPosition) + itemHolder.ev(editItemConfig.itemEditTextViewId)?.clearListeners() } -} - -/**输入框样式配置*/ -class EditStyleConfig : TextStyleConfig() { - - /**文本输入类型*/ - var editInputType = InputType.TYPE_CLASS_TEXT - - /**最大输入字符数*/ - var editMaxInputLength = DslBaseEditItem.DEFAULT_MAX_INPUT_LENGTH - - /**输入过滤器*/ - var editInputFilterList = mutableListOf() - - /**输入框不可编辑*/ - var noEditModel: Boolean = false - - /**最大输入行数, <=1 单行*/ - var editMaxLine: Int = 1 - set(value) { - field = value - textGravity = if (value <= 1) { - Gravity.LEFT or Gravity.CENTER_VERTICAL - } else { - Gravity.TOP or Gravity.LEFT - } - } - - override fun updateStyle(textView: TextView) { - super.updateStyle(textView) - - with(textView) { - //清空text change监听 - clearListeners() - - //过滤器 - filters = editInputFilterList.toTypedArray() - - //单行 or 多行 - setMaxLine(editMaxLine) - - inputType = editInputType - isEnabled = !noEditModel - addFilter(InputFilter.LengthFilter(editMaxInputLength)) - } + override fun onItemChangeListener(item: DslAdapterItem) { + //super.onItemChangeListener(item) + updateItemOnHaveDepend() } } \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/DslLabelEditItem.kt b/dslitem/src/main/java/com/angcyo/item/DslLabelEditItem.kt index a568788..0edd5c9 100644 --- a/dslitem/src/main/java/com/angcyo/item/DslLabelEditItem.kt +++ b/dslitem/src/main/java/com/angcyo/item/DslLabelEditItem.kt @@ -1,5 +1,6 @@ package com.angcyo.item +import android.graphics.drawable.Drawable import android.view.View import com.angcyo.dsladapter.DslAdapterItem import com.angcyo.dsladapter.DslViewHolder @@ -13,9 +14,25 @@ import com.angcyo.dsladapter.DslViewHolder */ open class DslLabelEditItem : DslBaseEditItem() { - /**编辑提示按钮*/ + /**编辑提示按钮, 负数会返回null对象*/ var itemEditTipIcon: Int = R.drawable.lib_icon_edit_tip + /**优先于属性[itemEditTipIcon]*/ + var itemEditTipDrawable: Drawable? = null + + /**右边图标点击事件, 如果设置回调. 会影响默认的事件处理*/ + var itemRightIcoClick: ((DslViewHolder, View) -> Unit)? = null + + /**右边的文本*/ + var itemRightText: CharSequence? = null + set(value) { + field = value + itemRightTextStyle.text = value + } + + /**统一样式配置*/ + var itemRightTextStyle = TextStyleConfig() + init { itemLayoutId = R.layout.dsl_label_edit_item } @@ -29,15 +46,35 @@ open class DslLabelEditItem : DslBaseEditItem() { super.onItemBind(itemHolder, itemPosition, adapterItem, payloads) itemHolder.img(R.id.lib_right_ico_view)?.apply { - if (itemEditTextStyle.noEditModel) { + val drawable = itemEditTipDrawable ?: loadDrawable(itemEditTipIcon) + if (drawable == null || editItemConfig.itemNoEditModel == true /*不可编辑*/) { gone() } else { visible() - clickIt { - itemHolder.focus(R.id.lib_edit_view)?.showSoftInput() + setImageDrawable(drawable) + } + + //处理默认弹出软键盘 + if (itemRightIcoClick == null) { + if (editItemConfig.itemEditTextStyle.noEditModel) { + gone() + } else { + throttleClickIt { + itemHolder.focus(editItemConfig.itemEditTextViewId)?.showSoftInput() + } + } + } else { + throttleClickIt { + itemRightIcoClick?.invoke(itemHolder, it) } } - setImageDrawable(loadDrawable(itemEditTipIcon)) + + } + + //右边文本, cm unit + itemHolder.gone(R.id.lib_right_text_view, itemRightTextStyle.text == null) + itemHolder.tv(R.id.lib_right_text_view)?.apply { + itemRightTextStyle.updateStyle(this) } } } \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/DslNestedRecyclerItem.kt b/dslitem/src/main/java/com/angcyo/item/DslNestedRecyclerItem.kt new file mode 100644 index 0000000..c53f1d3 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/DslNestedRecyclerItem.kt @@ -0,0 +1,41 @@ +package com.angcyo.item + +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import com.angcyo.dsladapter.item.IFragmentItem +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.item.style.INestedRecyclerItem +import com.angcyo.item.style.NestedRecyclerItemConfig +import com.angcyo.dsladapter.DslViewHolder + +/** + * 内嵌[RecyclerView]的item + * Email:angcyo@126.com + * @author angcyo + * @date 2020/03/19 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +open class DslNestedRecyclerItem : DslAdapterItem(), INestedRecyclerItem, IFragmentItem { + + override var nestedRecyclerItemConfig: NestedRecyclerItemConfig = NestedRecyclerItemConfig() + + override var itemFragment: Fragment? = null + + init { + itemLayoutId = R.layout.dsl_nested_recycler_item + } + + override fun onItemBind( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + super.onItemBind(itemHolder, itemPosition, adapterItem, payloads) + } + + override fun onItemViewRecycled(itemHolder: DslViewHolder, itemPosition: Int) { + super.onItemViewRecycled(itemHolder, itemPosition) + onNestedRecyclerViewRecycled(itemHolder, itemPosition) + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/LibEx.kt b/dslitem/src/main/java/com/angcyo/item/LibEx.kt index 1384a59..3cec3ce 100644 --- a/dslitem/src/main/java/com/angcyo/item/LibEx.kt +++ b/dslitem/src/main/java/com/angcyo/item/LibEx.kt @@ -9,18 +9,28 @@ import android.text.InputFilter import android.text.TextPaint import android.text.TextUtils import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText +import android.widget.OverScroller import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes +import androidx.annotation.DimenRes import androidx.annotation.DrawableRes +import androidx.annotation.IdRes import androidx.annotation.LayoutRes +import androidx.annotation.Px import androidx.core.content.ContextCompat +import androidx.core.widget.ScrollerCompat +import androidx.core.widget.TextViewCompat +import com.angcyo.dsladapter.DslViewHolder import com.angcyo.dsladapter.L +import com.angcyo.dsladapter.internal.ThrottleClickListener import com.angcyo.item.base.LibInitProvider +import com.angcyo.widget.DslButton import com.angcyo.widget.edit.SingleTextWatcher import com.angcyo.widget.span.undefined_int import java.util.* @@ -52,6 +62,17 @@ fun Paint.addPaintFlags(add: Boolean, flat: Int) { } } +/**文本的宽度*/ +fun Paint.textWidth(text: String?): Float { + if (text == null) { + return 0f + } + return measureText(text) +} + +/**文本的高度*/ +fun Paint?.textHeight(): Float = this?.run { descent() - ascent() } ?: 0f + /** * 设置是否加粗文本 */ @@ -179,6 +200,13 @@ fun EditText.onTextChange( }) } +/** + * 从一个对象中, 获取指定的成员对象 + */ +fun Any?.getMember(member: String): Any? { + return this?.run { this.getMember(this.javaClass, member) } +} + fun Any?.getMember( cls: Class<*>, member: String @@ -194,6 +222,28 @@ fun Any?.getMember( return result } +fun Any?.getCurrVelocity(): Float { + return when (this) { + is OverScroller -> currVelocity + is ScrollerCompat -> currVelocity + else -> { + 0f + } + } +} + +fun MotionEvent.isTouchDown(): Boolean { + return actionMasked == MotionEvent.ACTION_DOWN +} + +fun MotionEvent.isTouchFinish(): Boolean { + return actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL +} + +fun MotionEvent.isTouchMove(): Boolean { + return actionMasked == MotionEvent.ACTION_MOVE +} + /**清空所有[TextWatcher]*/ fun TextView.clearListeners() { try { @@ -218,6 +268,14 @@ fun TextView?.setMaxLine(maxLine: Int = 1) { } } +/**设置文本, 并且将光标至于文本最后面*/ +fun TextView.setInputText(text: CharSequence? = null, selection: Boolean = true) { + setText(text) + if (selection && this is EditText) { + setSelection(min(text?.length ?: 0, getText().length)) + } +} + fun View?.padding(p: Int) { this?.setPadding(p, p, p, p) } @@ -227,6 +285,21 @@ fun _color(@ColorRes id: Int): Int { return getColor(id) } +@Px +fun _dimen(@DimenRes id: Int, context: Context = LibInitProvider.contentProvider): Int { + return getDimen(id, context) +} + +@Px +fun getDimen(@DimenRes id: Int, context: Context = LibInitProvider.contentProvider): Int { + return context.getDimen(id) +} + +@Px +fun Context.getDimen(@DimenRes id: Int): Int { + return resources.getDimensionPixelOffset(id) +} + @ColorInt fun getColor(@ColorRes id: Int): Int { return ContextCompat.getColor(LibInitProvider.contentProvider, id) @@ -240,6 +313,33 @@ fun View?.gone(value: Boolean = true) { this?.visibility = if (value) View.GONE else View.VISIBLE } +fun Int.getSize(): Int { + return View.MeasureSpec.getSize(this) +} + +fun Int.getMode(): Int { + return View.MeasureSpec.getMode(this) +} + +/**match_parent*/ +fun Int.isExactly(): Boolean { + return getMode() == View.MeasureSpec.EXACTLY +} + +/**wrap_content*/ +fun Int.isAtMost(): Boolean { + return getMode() == View.MeasureSpec.AT_MOST +} + +fun Int.isUnspecified(): Boolean { + return getMode() == View.MeasureSpec.UNSPECIFIED +} + +/**未指定大小*/ +fun Int.isNotSpecified(): Boolean { + return isAtMost() || isUnspecified() +} + /**点击事件*/ fun View?.clickIt(action: (View) -> Unit) { this?.setOnClickListener(action) @@ -268,6 +368,29 @@ fun TextView.addFilter(filter: InputFilter) { filters = newFilters } +/**移除指定[InputFilter]*/ +fun TextView.removeFilter(predicate: InputFilter.() -> Boolean) { + val oldFilters = filters + val removeList = mutableListOf() + oldFilters.forEach { + if (it.predicate()) { + removeList.add(it) + } + } + if (removeList.isEmpty()) { + return + } + val list = oldFilters.toMutableList().apply { + removeAll(removeList) + } + filters = list.toTypedArray() +} + +fun TextView.leftIco() = TextViewCompat.getCompoundDrawablesRelative(this)[0] +fun TextView.topIco() = TextViewCompat.getCompoundDrawablesRelative(this)[1] +fun TextView.rightIco() = TextViewCompat.getCompoundDrawablesRelative(this)[2] +fun TextView.bottomIco() = TextViewCompat.getCompoundDrawablesRelative(this)[3] + /**恢复选中范围*/ fun EditText.restoreSelection(start: Int, stop: Int) { val length = text.length @@ -290,4 +413,50 @@ fun EditText.restoreSelection(start: Int, stop: Int) { } else if (_start >= 0) { setSelection(_start) } -} \ No newline at end of file +} + +fun Collection<*>?.size() = this?.size ?: 0 + + +/**判断2个列表中的数据是否改变过*/ +fun Collection?.isChange(other: List?): Boolean { + if (this.size() != other.size()) { + return true + } + this?.forEachIndexed { index, t -> + if (t != other?.getOrNull(index)) { + return true + } + } + return false +} + +fun DslViewHolder.button(@IdRes id: Int): DslButton? = v(id) + +fun Any?.string(def: CharSequence = ""): CharSequence { + return when { + this == null -> return def + this is TextView -> text ?: def + this is CharSequence -> this + else -> this.toString() + } +} + +fun Any.toStr(): String = when (this) { + is String -> this + else -> toString() +} + +/**点击事件节流处理*/ +fun View?.throttleClickIt(action: (View) -> Unit) { + this?.setOnClickListener(ThrottleClickListener(action = action)) +} + +val View.drawLeft get() = paddingLeft +val View.drawTop get() = paddingTop +val View.drawRight get() = measuredWidth - paddingRight +val View.drawBottom get() = measuredHeight - paddingBottom +val View.drawWidth get() = drawRight - drawLeft +val View.drawHeight get() = drawBottom - drawTop +val View.drawCenterX get() = drawLeft + drawWidth / 2 +val View.drawCenterY get() = drawTop + drawHeight / 2 \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/RAnimatorListener.kt b/dslitem/src/main/java/com/angcyo/item/RAnimatorListener.kt new file mode 100644 index 0000000..b4514c8 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/RAnimatorListener.kt @@ -0,0 +1,46 @@ +package com.angcyo.item + +import android.animation.Animator + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/31 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +open class RAnimatorListener : Animator.AnimatorListener { + + private var isCancel = false + + var onAnimatorFinish: (animator: Animator, fromCancel: Boolean) -> Unit = + { animator, fromCancel -> + _onAnimatorFinish(animator, fromCancel) + } + + override fun onAnimationRepeat(animation: Animator) { + } + + override fun onAnimationEnd(animation: Animator) { + if (isCancel) { + //当动画被取消的时候, 系统会回调onAnimationCancel, 然后 onAnimationEnd + //所以, 这里过滤一下 + } else { + onAnimatorFinish(animation, false) + } + } + + override fun onAnimationCancel(animation: Animator) { + isCancel = true + onAnimatorFinish(animation, true) + } + + override fun onAnimationStart(animation: Animator) { + isCancel = false + } + + open fun _onAnimatorFinish(animator: Animator, fromCancel: Boolean) { + + } + +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ButtonStyleConfig.kt b/dslitem/src/main/java/com/angcyo/item/style/ButtonStyleConfig.kt new file mode 100644 index 0000000..5a0f7ef --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ButtonStyleConfig.kt @@ -0,0 +1,145 @@ +package com.angcyo.item.style + +import android.graphics.Color +import android.view.View +import com.angcyo.dpi +import com.angcyo.item.R +import com.angcyo.item._color +import com.angcyo.widget.DslButton +import com.angcyo.widget.colorState +import com.angcyo.widget.span.undefined_float + +/** + * 按钮样式配置 + * Email:angcyo@126.com + * @author angcyo + * @date 2020/06/09 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +open class ButtonStyleConfig : TextStyleConfig() { + + companion object { + //主题渐变样式, 标准的填充色按钮 + val BUTTON_STYLE_THEME = 1 + + //填充颜色的样式, 无渐变, 纯色填充+波纹效果 + val BUTTON_STYLE_SOLID = 2 + + //边框->主题渐变填充 + val BUTTON_STYLE_FILL = 3 + } + + /**特定样式*/ + var style: Int = BUTTON_STYLE_THEME + + /**样式[BUTTON_STYLE_THEME] [BUTTON_STYLE_FILL]时, 渐变的颜色*/ + var styleThemeColors = intArrayOf( + _color(R.color.colorPrimary), + _color(R.color.colorPrimaryDark) + ) + + /**样式[BUTTON_STYLE_THEME] [BUTTON_STYLE_FILL]时, 文本的颜色*/ + var styleThemeTextColor = Color.WHITE + + /**样式[BUTTON_STYLE_SOLID]时, solid的颜色*/ + var styleSolidSolidColor = Color.WHITE + + /**样式[BUTTON_STYLE_FILL]时, solid的颜色*/ + var styleFillSolidColor = Color.TRANSPARENT + + /**样式[BUTTON_STYLE_SOLID] [BUTTON_STYLE_FILL]时, 文本的颜色*/ + var styleSolidTextColor = _color(R.color.text_general_color) + set(value) { + field = value + textColor = value + } + + /**[BUTTON_STYLE_FILL]*/ + var styleFillStrokeWidth = 2 * dpi + + /**[BUTTON_STYLE_FILL]*/ + var styleFillStrokeColor = _color(R.color.colorPrimary) + + var buttonRadius: Float = undefined_float + + init { + themeStyle() + } + + override fun updateStyle(view: View) { + super.updateStyle(view) + + if (view is DslButton) { + + //不支持不同圆角大小的样式 + if (buttonRadius == undefined_float) { + buttonRadius = view.normalRadii.first() + } + view.setButtonRadius(buttonRadius) + + when (style) { + BUTTON_STYLE_THEME -> { + view.enableTextStyle = true + view.setButtonGradientColors(styleThemeColors) + view.setButtonTextColor(textColor) + view.setButtonStrokeWidth(0) + } + + BUTTON_STYLE_SOLID -> { + view.enableTextStyle = true + view.setButtonGradientColors(null) + view.setButtonSolidColor(styleSolidSolidColor) + view.setButtonTextColor(textColor) + view.setButtonStrokeWidth(0) + } + + BUTTON_STYLE_FILL -> { + view.enableTextStyle = false + view.setButtonGradientColors(null) + view.setButtonSolidColor(styleFillSolidColor) + view.setButtonStrokeWidth(styleFillStrokeWidth) + view.setButtonStrokeColor(styleFillStrokeColor) + + view.pressStrokeWidth = 0 + view.pressGradientColors = styleThemeColors + } + } + + view.updateDrawable() + } + } + + /**使用主题样式, 颜色渐变*/ + fun themeStyle() { + style = BUTTON_STYLE_THEME + textColor = styleThemeTextColor + } + + /**使用渐变样式*/ + fun gradientStyle(gradientStartColor: Int, gradientEndColor: Int = gradientStartColor) { + themeStyle() + styleThemeColors = intArrayOf(gradientStartColor, gradientEndColor) + } + + /**填充颜色的样式*/ + fun solidStyle() { + style = BUTTON_STYLE_SOLID + textColor = styleSolidTextColor + } + + /**边框 填充样式*/ + fun fillStyle() { + style = BUTTON_STYLE_FILL + textColors = colorState( + DslButton.ATTR_PRESSED to styleThemeTextColor, + DslButton.ATTR_NORMAL to styleThemeColors[0] + ) + } + + /**白底黑字样式*/ + fun whiteStyle() { + style = BUTTON_STYLE_SOLID + textColor = styleSolidTextColor + styleSolidSolidColor = Color.WHITE + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/EditStyleConfig.kt b/dslitem/src/main/java/com/angcyo/item/style/EditStyleConfig.kt new file mode 100644 index 0000000..534b8d5 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/EditStyleConfig.kt @@ -0,0 +1,97 @@ +package com.angcyo.item.style + +import android.text.InputFilter +import android.text.InputType +import android.text.method.DigitsKeyListener +import android.view.Gravity +import android.view.View +import android.widget.EditText +import android.widget.TextView +import com.angcyo.item.DslBaseEditItem +import com.angcyo.item.add +import com.angcyo.item.addFilter +import com.angcyo.item.clearListeners +import com.angcyo.item.remove +import com.angcyo.item.removeFilter +import com.angcyo.item.setInputText +import com.angcyo.item.setMaxLine +import com.angcyo.widget.CharLengthFilter + +/** + * 输入框样式配置 + * Email:angcyo@126.com + * @author angcyo + * @date 2020/06/09 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ + +class EditStyleConfig : TextStyleConfig() { + + /**文本输入类型*/ + var editInputType = InputType.TYPE_CLASS_TEXT + + /**最大输入字符数, -1取消限制*/ + var editMaxInputLength = DslBaseEditItem.DEFAULT_MAX_INPUT_LENGTH + + /**输入过滤器*/ + var editInputFilterList = mutableListOf() + + /**输入限制, 此属性和[editInputFilterList]互斥 + * [R.string.lib_number_digits] + * [R.string.lib_password_digits] + * [R.string.lib_en_digits]*/ + var editDigits: String? = null + + /**输入框不可编辑*/ + var noEditModel: Boolean = false + + /**最大输入行数, <=1 单行*/ + var editMaxLine: Int = 1 + set(value) { + field = value + if (value <= 1) { + textGravity = Gravity.LEFT or Gravity.CENTER_VERTICAL + editInputType = editInputType.remove(InputType.TYPE_TEXT_FLAG_MULTI_LINE) + } else { + textGravity = Gravity.TOP or Gravity.LEFT + editInputType = editInputType.add(InputType.TYPE_TEXT_FLAG_MULTI_LINE) + } + } + + override fun updateStyle(view: View) { + super.updateStyle(view) + + if (view is TextView) { + with(view) { + //清空text change监听 + clearListeners() + + //过滤器 + filters = editInputFilterList.toTypedArray() + + //单行 or 多行 + setMaxLine(editMaxLine) + + inputType = editInputType + isEnabled = !noEditModel + + if (editMaxInputLength > 0) { + addFilter(InputFilter.LengthFilter(editMaxInputLength)) + } else { + removeFilter { + this is InputFilter.LengthFilter || this is CharLengthFilter + } + } + + //digits 放在[inputType]后面 + editDigits?.let { + keyListener = DigitsKeyListener.getInstance(it) + } + + if (this is EditText) { + setInputText(this@EditStyleConfig.text) + } + } + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/IAutoInitItem.kt b/dslitem/src/main/java/com/angcyo/item/style/IAutoInitItem.kt new file mode 100644 index 0000000..2f82330 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/IAutoInitItem.kt @@ -0,0 +1,66 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItem + +/** + * 自动初始化, 继承此类的item, 可以实现自动初始化 + * [com.angcyo.dsladapter.DslAdapterItem._initItemConfig] + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IAutoInitItem : IDslItem { + + /**自动初始化入口, 统一入口*/ + @ItemInitEntryPoint + override fun initItemConfig( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + super.initItemConfig(itemHolder, itemPosition, adapterItem, payloads) + + //分发具体的初始化方法 + if (this is IBadgeItem) { + initBadgeItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is IDesItem) { + initDesItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is ITextItem) { + initTextItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is IEditItem) { + initEditItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is ILabelItem) { + initLabelItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is ITextInfoItem) { + initInfoTextItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is IBodyItem) { + initBodyItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is IButtonItem) { + initButtonItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is INestedRecyclerItem) { + initNestedRecyclerItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is ICheckItem) { + initCheckItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is ISwitchItem) { + initSwitchItem(itemHolder, itemPosition, adapterItem, payloads) + } + if (this is ICheckGroupItem) { + initCheckGroupItem(itemHolder, itemPosition, adapterItem, payloads) + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/IBadgeItem.kt b/dslitem/src/main/java/com/angcyo/item/style/IBadgeItem.kt new file mode 100644 index 0000000..54f75ef --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/IBadgeItem.kt @@ -0,0 +1,54 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.widget.IBadgeView + +/** + * 角标文本[BadgeTextView]item + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IBadgeItem : IAutoInitItem { + + var badgeItemConfig: BadgeItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initBadgeItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.view(badgeItemConfig.itemBadgeViewId)?.apply { + if (this is IBadgeView) { + dslBadeDrawable.apply { + drawBadge = true + badgeText = badgeItemConfig.itemBadgeText + requestLayout() + } + } + } + } +} + +var IBadgeItem.itemBadgeText: String? + get() = badgeItemConfig.itemBadgeText + set(value) { + badgeItemConfig.itemBadgeText = value + } + + +class BadgeItemConfig : IDslItemConfig { + + var itemBadgeViewId: Int = R.id.lib_badge_view + + /**[com.angcyo.drawable.text.DslBadgeDrawable.badgeText]*/ + var itemBadgeText: String? = null +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/IBodyItem.kt b/dslitem/src/main/java/com/angcyo/item/style/IBodyItem.kt new file mode 100644 index 0000000..eda5bb1 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/IBodyItem.kt @@ -0,0 +1,56 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.dsladapter.DslViewHolder + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IBodyItem : IAutoInitItem { + + /**统一样式配置*/ + var bodyItemConfig: BodyItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initBodyItem(itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List) { + itemHolder.tv(bodyItemConfig.itemBodyViewId)?.apply { + bodyItemConfig.itemBodyStyle.updateStyle(this) + } + } + + fun configBodyStyle(action: TextStyleConfig.() -> Unit) { + bodyItemConfig.itemBodyStyle.action() + } +} + +var IBodyItem.itemBodyText: CharSequence? + get() = bodyItemConfig.itemBodyText + set(value) { + bodyItemConfig.itemBodyText = value + } + +class BodyItemConfig : IDslItemConfig { + + var itemBodyViewId: Int = R.id.lib_body_view + + /**文本*/ + var itemBodyText: CharSequence? = null + set(value) { + field = value + itemBodyStyle.text = value + } + + /**统一样式配置*/ + var itemBodyStyle = TextStyleConfig() +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/IButtonItem.kt b/dslitem/src/main/java/com/angcyo/item/style/IButtonItem.kt new file mode 100644 index 0000000..99ba97b --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/IButtonItem.kt @@ -0,0 +1,72 @@ +package com.angcyo.item.style + +import android.view.Gravity +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.widget.DslButton +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.item.button + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/24 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IButtonItem : IAutoInitItem { + + var buttonItemConfig: ButtonItemConfig + + @ItemInitEntryPoint + fun initButtonItem(itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List) { + itemHolder.itemView.isClickable = false + + itemHolder.button(buttonItemConfig.itemButtonViewId)?.apply { + buttonItemConfig.itemButtonStyle.updateStyle(this) + buttonItemConfig.itemButtonConfig(this) + + if (this@IButtonItem is DslAdapterItem) { + setOnClickListener(_clickListener) + setOnLongClickListener(_longClickListener) + } + } + } + + fun configButtonStyle(action: ButtonStyleConfig.() -> Unit) { + buttonItemConfig.itemButtonStyle.action() + } +} + +var IButtonItem.itemButtonText: CharSequence? + get() = buttonItemConfig.itemButtonText + set(value) { + buttonItemConfig.itemButtonText = value + } + +class ButtonItemConfig : IDslItemConfig { + + var itemButtonViewId: Int = R.id.lib_button + + /**按钮显示的文本*/ + var itemButtonText: CharSequence? = null + set(value) { + field = value + itemButtonStyle.text = value + } + + /**按钮样式配置项*/ + var itemButtonStyle = ButtonStyleConfig().apply { + textGravity = Gravity.CENTER + } + + /**按钮配置回调*/ + var itemButtonConfig: (DslButton) -> Unit = { + + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ICheckGroupItem.kt b/dslitem/src/main/java/com/angcyo/item/style/ICheckGroupItem.kt new file mode 100644 index 0000000..079f1d2 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ICheckGroupItem.kt @@ -0,0 +1,209 @@ +package com.angcyo.item.style + +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.extend.IToText +import com.angcyo.extend.IToValue +import com.angcyo.item.R +import com.angcyo.item.string +import com.angcyo.item.toStr +import com.angcyo.widget.DslSelector +import com.angcyo.widget.find +import com.angcyo.widget.resetChild + +/** + * @author angcyo + * @since 2022/06/20 + */ +interface ICheckGroupItem : IAutoInitItem { + + /**配置项*/ + var checkGroupItemConfig: CheckGroupItemConfig + + @ItemInitEntryPoint + fun initCheckGroupItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.v(checkGroupItemConfig.itemCheckGroupViewId) + ?.apply { + + resetChild( + checkGroupItemConfig.itemCheckItems.size, + checkGroupItemConfig.itemCheckLayoutId + ) { itemView, itemIndex -> + val item = checkGroupItemConfig.itemCheckItems[itemIndex] + itemView.tag = checkGroupItemConfig.itemCheckItemToValue(item) //保存数据 + itemView.find(R.id.lib_text_view)?.text = + checkGroupItemConfig.itemCheckItemToText(item) + } + + /**安装选择组件*/ + checkGroupItemConfig.itemSelectorHelper.install(this) { + dslMultiMode = checkGroupItemConfig.itemMultiMode + dslMinSelectLimit = + if (checkGroupItemConfig.itemMultiMode) checkGroupItemConfig.itemMinSelectLimit else 1 + + onSelectItemView = this@ICheckGroupItem::onCheckInterceptSelectView + onSelectViewChange = this@ICheckGroupItem::onCheckSelectViewChange + onSelectIndexChange = this@ICheckGroupItem::onCheckSelectIndexChange + } + + val indexList = mutableListOf() + checkGroupItemConfig.itemCheckedItems.forEach { + indexList.add(checkGroupItemConfig.itemCheckItems.indexOf(it)) + } + checkGroupItemConfig.itemSelectorHelper.selector( + indexList, + fromUser = checkGroupItemConfig.itemFirstNotifyChange + ) + checkGroupItemConfig.itemFirstNotifyChange = false + } + } + + /**是否需要拦截选中*/ + fun onCheckInterceptSelectView( + itemView: View, + index: Int, + select: Boolean, + fromUser: Boolean + ): Boolean { + return false + } + + /**选中后的view改变的回调*/ + fun onCheckSelectViewChange( + fromView: View?, + selectViewList: List, + reselect: Boolean, + fromUser: Boolean + ) { + + } + + /**选中后的index改变的回调*/ + fun onCheckSelectIndexChange( + fromIndex: Int, + selectIndexList: List, + reselect: Boolean, + fromUser: Boolean + ) { + checkGroupItemConfig._itemCheckedIndexList.clear() + checkGroupItemConfig._itemCheckedIndexList.addAll(selectIndexList) + + //清空之前 + checkGroupItemConfig.itemCheckedItems.clear() + + //当前选中项 + selectIndexList.forEach { + checkGroupItemConfig.itemCheckedItems.add(checkGroupItemConfig.itemCheckItems[it]) + } + + //回调 + checkGroupItemConfig.itemCheckChangedAction(fromIndex, selectIndexList, reselect, fromUser) + + //更新依赖 + if (fromUser) { + if (this is DslAdapterItem) { + itemChanging = true + } + } + } + + /**Dsl*/ + fun configCheckGroupItem(block: CheckGroupItemConfig.() -> Unit) { + checkGroupItemConfig.block() + } +} + +/**需要选择的项*/ +var ICheckGroupItem.itemCheckItems: List + get() = checkGroupItemConfig.itemCheckItems + set(value) { + checkGroupItemConfig.itemCheckItems = value + } + +/**选中的项*/ +var ICheckGroupItem.itemCheckedItems: MutableList + get() = checkGroupItemConfig.itemCheckedItems + set(value) { + checkGroupItemConfig.itemCheckedItems = value + } + +/**只读属性, 选中的索引值*/ +val ICheckGroupItem._itemCheckedIndexList: List + get() = checkGroupItemConfig._itemCheckedIndexList + +/**布局id*/ +var ICheckGroupItem.itemCheckLayoutId: Int + get() = checkGroupItemConfig.itemCheckLayoutId + set(value) { + checkGroupItemConfig.itemCheckLayoutId = value + } + +/**回调*/ +var ICheckGroupItem.itemCheckChangedAction: (fromIndex: Int, selectIndexList: List, reselect: Boolean, fromUser: Boolean) -> Unit + get() = checkGroupItemConfig.itemCheckChangedAction + set(value) { + checkGroupItemConfig.itemCheckChangedAction = value + } + +class CheckGroupItemConfig : IDslItemConfig { + + /**需要操作的控件id[R.id.lib_flow_layout]*/ + var itemCheckGroupViewId: Int = R.id.lib_flow_layout + + /**选项列表*/ + var itemCheckItems = listOf() + + /**选中的列表*/ + var itemCheckedItems = mutableListOf() + + /**选项布局*/ + var itemCheckLayoutId: Int = R.layout.layout_check + + /**是否是多选模式*/ + var itemMultiMode = false + + /**多选时, 最小选中数量*/ + var itemMinSelectLimit = 0 + + /**将选项[item], 转成可以显示在界面的 文本类型*/ + var itemCheckItemToText: (item: Any) -> CharSequence? = { item -> + if (item is IToText) { + item.toText() + } else { + item.string() + } + } + + /**将选项[item], 转成表单上传的数据*/ + var itemCheckItemToValue: (item: Any) -> Any? = { item -> + if (item is IToValue) { + item.toValue() + } else { + item.toStr() + } + } + + var itemFirstNotifyChange = true + + /**回调*/ + var itemCheckChangedAction: (fromIndex: Int, selectIndexList: List, reselect: Boolean, fromUser: Boolean) -> Unit = + { fromIndex, selectIndexList, reselect, fromUser -> + + } + + /**单选/多选支持*/ + val itemSelectorHelper = DslSelector() + + //内部使用 + var _itemCheckedIndexList = mutableListOf() +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ICheckItem.kt b/dslitem/src/main/java/com/angcyo/item/style/ICheckItem.kt new file mode 100644 index 0000000..d26ea06 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ICheckItem.kt @@ -0,0 +1,90 @@ +package com.angcyo.item.style + +import android.view.View +import android.widget.CompoundButton +import android.widget.ImageView +import com.angcyo.dsladapter.* +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.dsladapter.DslViewHolder + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2022/02/25 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface ICheckItem : IAutoInitItem { + + /**配置项*/ + var checkItemConfig: CheckItemConfig + + @ItemInitEntryPoint + fun initCheckItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + val item = this + itemHolder.view(checkItemConfig.itemCheckViewId)?.apply { + when (this) { + is ImageView -> setImageResource(checkItemConfig.itemCheckResId) + is CompoundButton -> setButtonDrawable(checkItemConfig.itemCheckResId) + else -> setBackgroundResource(checkItemConfig.itemCheckResId) + } + } + + //[DslAdapterItem] + if (item is DslAdapterItem) { + item.itemDslAdapter?.apply { + if (itemSelectorHelper.selectorModel == ItemSelectorHelper.MODEL_NORMAL) { + //开启多选模式 + multiModel() + } + } + item.itemClick = this::onItemClick + item.onItemSelectorChange = this::onItemSelectorChange + + //重新初始化一下Listener + item._initItemListener(itemHolder) + + //状态 + itemHolder.selected(checkItemConfig.itemCheckViewId, item.itemIsSelected) + } + } + + /**点击事件的绑定*/ + fun onItemClick(view: View) { + val item = this + if (item is DslAdapterItem) { + //切换选中状态 + item.select { + //不通知界面更新, 因为如果使用此方式更新界面, item原本的背景波纹效果会丢失 + notifyItemChanged = false + } + } + } + + /**选中状态改变后, 通过本地更新的方式更新界面*/ + fun onItemSelectorChange(selectorParams: SelectorParams) { + val item = this + if (item is DslAdapterItem) { + item.itemViewHolder()?.apply { + selected(checkItemConfig.itemCheckViewId, item.itemIsSelected) + } + } + } +} + +class CheckItemConfig : IDslItemConfig { + + /**需要操作的控件id[R.id.lib_check_view]*/ + var itemCheckViewId: Int = R.id.lib_check_view + + /**选中状态提示资源*/ + var itemCheckResId: Int = R.drawable.lib_check_selector + +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/IDesItem.kt b/dslitem/src/main/java/com/angcyo/item/style/IDesItem.kt new file mode 100644 index 0000000..b6241ed --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/IDesItem.kt @@ -0,0 +1,56 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.dsladapter.DslViewHolder + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IDesItem : IAutoInitItem { + + /**统一样式配置*/ + var desItemConfig: DesItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initDesItem(itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List) { + itemHolder.tv(desItemConfig.itemDesViewId)?.apply { + desItemConfig.itemDesStyle.updateStyle(this) + } + } + + fun configDesStyle(action: TextStyleConfig.() -> Unit) { + desItemConfig.itemDesStyle.action() + } +} + +var IDesItem.itemDes: CharSequence? + get() = desItemConfig.itemDes + set(value) { + desItemConfig.itemDes = value + } + +class DesItemConfig : IDslItemConfig { + + var itemDesViewId: Int = R.id.lib_des_view + + /**文本*/ + var itemDes: CharSequence? = null + set(value) { + field = value + itemDesStyle.text = value + } + + /**统一样式配置*/ + var itemDesStyle = TextStyleConfig() +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/IEditItem.kt b/dslitem/src/main/java/com/angcyo/item/style/IEditItem.kt new file mode 100644 index 0000000..c0a2313 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/IEditItem.kt @@ -0,0 +1,171 @@ +package com.angcyo.item.style + +import android.text.InputFilter +import android.text.InputType +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.DslBaseEditItem +import com.angcyo.item.R +import com.angcyo.item.clearListeners +import com.angcyo.item.onTextChange +import com.angcyo.item.restoreSelection + +/** + * 输入框item + * Email:angcyo@126.com + * @author angcyo + * @date 2021/06/28 + * Copyright (c) 2020 angcyo. All rights reserved. + */ + +/**文本改变通知回调*/ +typealias TextChangeAction = (CharSequence) -> Unit + +interface IEditItem : IAutoInitItem { + + /**配置项*/ + var editItemConfig: EditItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initEditItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.ev(editItemConfig.itemEditTextViewId)?.apply { + clearListeners() + + //[EditStyleConfig]样式初始化 + editItemConfig.itemEditTextStyle.updateStyle(this) + + onTextChange { + editItemConfig._lastEditSelectionStart = selectionStart + editItemConfig._lastEditSelectionEnd = selectionEnd + + editItemConfig.itemEditText = it + } + + //放在最后监听, 防止首次setInputText, 就触发事件. + onTextChange(shakeDelay = editItemConfig.itemTextChangeShakeDelay) { + if (this@IEditItem is DslAdapterItem) { + itemChanging = true + } + onItemEditTextChange(itemHolder, it) + } + + restoreSelection( + editItemConfig._lastEditSelectionStart, + editItemConfig._lastEditSelectionEnd + ) + } + } + + fun configEditTextStyle(action: EditStyleConfig.() -> Unit) { + editItemConfig.itemEditTextStyle.action() + } + + /**清除之前的监听*/ + fun clearEditListeners(itemHolder: DslViewHolder) { + itemHolder.ev(editItemConfig.itemEditTextViewId)?.clearListeners() + } + + /**编辑的文本改变后*/ + fun onItemEditTextChange(itemHolder: DslViewHolder, text: CharSequence) { + editItemConfig.itemTextChangeAction?.invoke(text) + } +} + +var IEditItem.itemEditText: CharSequence? + get() = editItemConfig.itemEditText + set(value) { + editItemConfig.itemEditText = value + } + +var IEditItem.itemEditHint: CharSequence? + get() = editItemConfig.itemEditTextStyle.hint + set(value) { + editItemConfig.itemEditTextStyle.hint = value + } + +/** + * 输入类型 + * [InputType.TYPE_CLASS_TEXT] + * [InputType.TYPE_CLASS_NUMBER] + * + * [InputType.TYPE_TEXT_FLAG_MULTI_LINE] + * + * [InputType.TYPE_NUMBER_FLAG_DECIMAL] + * [InputType.TYPE_NUMBER_FLAG_SIGNED] + * */ +var IEditItem.itemEditInputType: Int + get() = editItemConfig.itemEditTextStyle.editInputType + set(value) { + editItemConfig.itemEditTextStyle.editInputType = value + } + + +var IEditItem.itemMaxInputLength: Int + get() = editItemConfig.itemEditTextStyle.editMaxInputLength + set(value) { + editItemConfig.itemEditTextStyle.editMaxInputLength = value + } + +/**输入过滤*/ +var IEditItem.itemInputFilterList: MutableList + get() = editItemConfig.itemEditTextStyle.editInputFilterList + set(value) { + editItemConfig.itemEditTextStyle.editInputFilterList = value + } + +/**输入过滤*/ +var IEditItem.itemEditDigits: String? + get() = editItemConfig.itemEditTextStyle.editDigits + set(value) { + editItemConfig.itemEditTextStyle.editDigits = value + } + +var IEditItem.itemTextChangeAction: TextChangeAction? + get() = editItemConfig.itemTextChangeAction + set(value) { + editItemConfig.itemTextChangeAction = value + } + +class EditItemConfig : IDslItemConfig { + + /**[R.id.lib_edit_view]*/ + var itemEditTextViewId: Int = R.id.lib_edit_view + + /**输入框内容*/ + var itemEditText: CharSequence? = null + set(value) { + field = value + itemEditTextStyle.text = value + } + + /**是否可编辑*/ + var itemNoEditModel: Boolean? = null + set(value) { + field = value + if (value == true) { + itemEditTextStyle.hint = null + } + } + + /**统一样式配置*/ + var itemEditTextStyle: EditStyleConfig = EditStyleConfig() + + /**文本改变*/ + var itemTextChangeAction: TextChangeAction? = null + + /**文本改变去频限制, 负数表示不开启, 如果短时间内关闭界面了, 可能会获取不到最新的输入框数据*/ + var itemTextChangeShakeDelay: Long = DslBaseEditItem.DEFAULT_INPUT_SHAKE_DELAY + + //用于恢复光标的位置 + var _lastEditSelectionStart: Int = -1 + + var _lastEditSelectionEnd: Int = -1 +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ILabelItem.kt b/dslitem/src/main/java/com/angcyo/item/style/ILabelItem.kt new file mode 100644 index 0000000..cd8c245 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ILabelItem.kt @@ -0,0 +1,74 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.dsladapter.DslViewHolder + +/** + * 带Label的item + * Email:angcyo@126.com + * @author angcyo + * @date 2021/06/28 + * Copyright (c) 2020 angcyo. All rights reserved. + */ +interface ILabelItem : IAutoInitItem { + + /**统一样式配置*/ + var labelItemConfig: LabelItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initLabelItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.tv(labelItemConfig.itemLabelViewId)?.apply { + labelItemConfig.itemLabelTextStyle.updateStyle(this) + } + } + + fun configLabelTextStyle(action: TextStyleConfig.() -> Unit) { + labelItemConfig.itemLabelTextStyle.action() + } +} + +var ILabelItem.itemLabelText: CharSequence? + get() = labelItemConfig.itemLabelText + set(value) { + labelItemConfig.itemLabelText = value + } + +var ILabelItem.itemLabel: CharSequence? + get() = labelItemConfig.itemLabelText + set(value) { + labelItemConfig.itemLabelText = value + } + +/**label视图的最小宽度*/ +var ILabelItem.itemLabelMinWidth: Int + get() = labelItemConfig.itemLabelTextStyle.viewMinWidth + set(value) { + configLabelTextStyle { + viewMinWidth = value + } + } + +class LabelItemConfig : IDslItemConfig { + + /**[R.id.lib_label_view]*/ + var itemLabelViewId: Int = R.id.lib_label_view + + /**Label文本*/ + var itemLabelText: CharSequence? = null + set(value) { + field = value + itemLabelTextStyle.text = value + } + + /**统一样式配置*/ + var itemLabelTextStyle: TextStyleConfig = TextStyleConfig() +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/INestedRecyclerItem.kt b/dslitem/src/main/java/com/angcyo/item/style/INestedRecyclerItem.kt new file mode 100644 index 0000000..a0e7409 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/INestedRecyclerItem.kt @@ -0,0 +1,197 @@ +package com.angcyo.item.style + +import androidx.recyclerview.widget.RecyclerView +import com.angcyo.dsladapter.DslAdapter +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.dsladapter.item.IFragmentItem +import com.angcyo.item.R +import com.angcyo.item.base.LibInitProvider +import com.angcyo.widget.recycler.* +import java.lang.ref.WeakReference + +/** + * 内嵌一个RecyclerView的Item + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/26 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface INestedRecyclerItem : IAutoInitItem { + + var nestedRecyclerItemConfig: NestedRecyclerItemConfig + + /**初始化[INestedRecyclerItem]*/ + @ItemInitEntryPoint + fun initNestedRecyclerItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.rv(nestedRecyclerItemConfig.itemNestedRecyclerViewId)?.apply { + onBindNestedRecyclerView(this, itemHolder, itemPosition, adapterItem, payloads) + } + } + + /**回收*/ + fun onNestedRecyclerViewRecycled(itemHolder: DslViewHolder, itemPosition: Int) { + itemHolder.rv(nestedRecyclerItemConfig.itemNestedRecyclerViewId)?.apply { + layoutManager = null + adapter = null + } + } + + /**绑定[RecyclerView]*/ + fun onBindNestedRecyclerView( + recyclerView: RecyclerView, + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + //列表 + recyclerView.apply { + //优先清空[OnScrollListener] + clearOnScrollListeners() + clearItemDecoration() + initDsl() + + if (layoutManager != nestedRecyclerItemConfig.itemNestedLayoutManager) { + layoutManager = nestedRecyclerItemConfig.itemNestedLayoutManager + } + + //关键地方, 如果每次都赋值[adapter], 系统会重置所有缓存. + if (adapter != nestedRecyclerItemConfig.itemNestedAdapter) { + adapter = nestedRecyclerItemConfig.itemNestedAdapter + } + + //配置 + nestedRecyclerItemConfig.itemNestedRecyclerViewConfig(this) + + //渲染数据 + if (adapter is DslAdapter) { + val dslAdapter = adapter as DslAdapter + dslAdapter._recyclerView = this + onRenderNestedAdapter(dslAdapter) + } + + //恢复滚动位置 + if (nestedRecyclerItemConfig.itemKeepScrollPosition) { + nestedRecyclerItemConfig._scrollPositionConfig?.run { restoreScrollPosition(this) } + } + + //记录滚动位置 + object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + nestedRecyclerItemConfig._scrollPositionConfig = saveScrollPosition() + } + }.apply { + nestedRecyclerItemConfig._onScrollListener = this + addOnScrollListener(this) + } + } + } + + /**开始界面渲染*/ + fun onRenderNestedAdapter(dslAdapter: DslAdapter) { + val item = this + dslAdapter.apply { + nestedRecyclerItemConfig.itemNestedItemList?.let { + clearItems() + if (item is DslAdapterItem) { + it.forEach { + if (it.itemParentRef?.get() == item) { + //一样的对象 + } else { + it.itemParentRef = WeakReference(item) + } + } + } + dataItems.addAll(it) + _updateAdapterItems() + } + + nestedRecyclerItemConfig.itemRenderNestedAdapter(this) + + if (this@INestedRecyclerItem is IFragmentItem) { + adapterItems.forEach { + if (it is IFragmentItem && it.itemFragment == null) { + it.itemFragment = this@INestedRecyclerItem.itemFragment + } + } + } + + /*if (item is DslAdapterItem && item.itemParentRef?.get() == null) { + updateNow() + } else { + notifyDataChanged() + }*/ + //强刷界面 + notifyDataChanged() + } + } +} + +var INestedRecyclerItem.itemNestedAdapter + get() = nestedRecyclerItemConfig.itemNestedAdapter + set(value) { + nestedRecyclerItemConfig.itemNestedAdapter = value + } + +var INestedRecyclerItem.itemNestedLayoutManager + get() = nestedRecyclerItemConfig.itemNestedLayoutManager + set(value) { + nestedRecyclerItemConfig.itemNestedLayoutManager = value + } + +fun INestedRecyclerItem.renderNestedAdapter(init: DslAdapter.() -> Unit) { + nestedRecyclerItemConfig.itemRenderNestedAdapter = init +} + +fun INestedRecyclerItem.addNestedItem(item: T, init: T.() -> Unit = {}) { + if (nestedRecyclerItemConfig.itemNestedItemList == null) { + nestedRecyclerItemConfig.itemNestedItemList = mutableListOf() + } + nestedRecyclerItemConfig.itemNestedItemList?.add(item) + item.init() +} + +class NestedRecyclerItemConfig : IDslItemConfig { + + var itemNestedRecyclerViewId: Int = R.id.lib_nested_recycler_view + + /**内嵌适配器*/ + var itemNestedAdapter: DslAdapter = DslAdapter().apply { + //关闭内部情感图状态 + dslAdapterStatusItem.itemEnable = false + } + + /**布局管理, + * 请注意使用属性:[recycleChildrenOnDetach]*/ + var itemNestedLayoutManager: RecyclerView.LayoutManager = + LinearLayoutManagerWrap(LibInitProvider.contentProvider).apply { + recycleChildrenOnDetach = true + } + + /**自动恢复滚动位置*/ + var itemKeepScrollPosition = true + + /**渲染内部[DslAdapter]数据*/ + var itemRenderNestedAdapter: DslAdapter.() -> Unit = {} + + /**简单的item数据集合, 如果使用了此属性, 请勿在[itemRenderNestedAdapter]重复渲染*/ + var itemNestedItemList: MutableList? = null + + /**内部[RecyclerView]配置回调*/ + var itemNestedRecyclerViewConfig: RecyclerView.() -> Unit = { + noItemAnim() + } + + var _onScrollListener: RecyclerView.OnScrollListener? = null + + var _scrollPositionConfig: ScrollPositionConfig? = null +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ISwitchItem.kt b/dslitem/src/main/java/com/angcyo/item/style/ISwitchItem.kt new file mode 100644 index 0000000..28bbe1a --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ISwitchItem.kt @@ -0,0 +1,82 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.github.SwitchButton +import com.angcyo.item.R +import com.angcyo.dsladapter.DslViewHolder + +/** + * [com.angcyo.github.SwitchButton] + * [com.angcyo.item.DslPropertySwitchItem] + * [com.angcyo.item.DslSwitchInfoItem] + * @author angcyo + * @since 2022/06/08 + */ +interface ISwitchItem : IAutoInitItem { + + /**统一样式配置*/ + var switchItemConfig: SwitchItemConfig + + @ItemInitEntryPoint + fun initSwitchItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.v(switchItemConfig.itemSwitchViewId)?.apply { + setOnCheckedChangeListener(null) + + //刷新界面的时候, 不执行动画 + val old = isEnableEffect + isEnableEffect = false + isChecked = itemSwitchChecked + isEnableEffect = old + + setOnCheckedChangeListener(object : SwitchButton.OnCheckedChangeListener { + override fun onCheckedChanged(view: SwitchButton, isChecked: Boolean) { + val checked = itemSwitchChecked + itemSwitchChecked = isChecked + if (checked != itemSwitchChecked) { + if (this@ISwitchItem is DslAdapterItem) { + itemChanging = true + } + switchItemConfig.itemSwitchChangedAction(itemSwitchChecked) + } + } + }) + } + } + + /**config*/ + fun configSwitchItem(action: SwitchItemConfig.() -> Unit) { + switchItemConfig.action() + } +} + +var ISwitchItem.itemSwitchChecked: Boolean + get() = switchItemConfig.itemSwitchChecked + set(value) { + switchItemConfig.itemSwitchChecked = value + } + +var ISwitchItem.itemSwitchChangedAction: (checked: Boolean) -> Unit + get() = switchItemConfig.itemSwitchChangedAction + set(value) { + switchItemConfig.itemSwitchChangedAction = value + } + +class SwitchItemConfig : IDslItemConfig { + + /**[R.id.lib_switch_view]*/ + var itemSwitchViewId: Int = R.id.lib_switch_view + + /**是否选中*/ + var itemSwitchChecked = false + + /**状态回调, 提供一个可以完全覆盖的方法*/ + var itemSwitchChangedAction: (checked: Boolean) -> Unit = { + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ITextInfoItem.kt b/dslitem/src/main/java/com/angcyo/item/style/ITextInfoItem.kt new file mode 100644 index 0000000..e90c1c6 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ITextInfoItem.kt @@ -0,0 +1,57 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.dsladapter.DslViewHolder + +/** + * 带Text的item + * Email:angcyo@126.com + * @author angcyo + * @date 2021/07/16 + * Copyright (c) 2020 angcyo. All rights reserved. + */ +interface ITextInfoItem : IAutoInitItem { + + var textInfoItemConfig: TextInfoItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initInfoTextItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.tv(textInfoItemConfig.itemInfoTextViewId)?.apply { + textInfoItemConfig.itemInfoTextStyle.updateStyle(this) + } + } + + fun configInfoTextStyle(action: TextStyleConfig.() -> Unit) { + textInfoItemConfig.itemInfoTextStyle.action() + } +} + +var ITextInfoItem.itemInfoText: CharSequence? + get() = textInfoItemConfig.itemInfoText + set(value) { + textInfoItemConfig.itemInfoText = value + } + +class TextInfoItemConfig : IDslItemConfig { + /**[R.id.lib_text_view]*/ + var itemInfoTextViewId: Int = R.id.lib_text_view + + /**条目文本*/ + var itemInfoText: CharSequence? = null + set(value) { + field = value + itemInfoTextStyle.text = value + } + + /**统一样式配置*/ + var itemInfoTextStyle: TextStyleConfig = TextStyleConfig() +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ITextItem.kt b/dslitem/src/main/java/com/angcyo/item/style/ITextItem.kt new file mode 100644 index 0000000..5afa57f --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ITextItem.kt @@ -0,0 +1,78 @@ +package com.angcyo.item.style + +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.annotation.ItemInitEntryPoint +import com.angcyo.dsladapter.item.IDslItemConfig +import com.angcyo.item.R +import com.angcyo.item._dimen +import com.angcyo.widget.span.undefined_float + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/18 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface ITextItem : IAutoInitItem { + + /**配置类 */ + var textItemConfig: TextItemConfig + + /**初始化*/ + @ItemInitEntryPoint + fun initTextItem( + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + itemHolder.tv(textItemConfig.itemTextViewId)?.apply { + textItemConfig.itemTextStyle.updateStyle(this) + } + } + + fun configTextStyle(action: TextStyleConfig.() -> Unit) { + textItemConfig.itemTextStyle.action() + } + + /**加粗样式*/ + fun boldStyle() { + configTextStyle { + textBold = true + if (textSize == undefined_float) { + textSize = _dimen(R.dimen.text_sub_size).toFloat() + } + } + } +} + +var ITextItem.itemText: CharSequence? + get() = textItemConfig.itemText + set(value) { + textItemConfig.itemText = value + } + +var ITextItem.itemHint: CharSequence? + get() = textItemConfig.itemTextStyle.hint + set(value) { + textItemConfig.itemTextStyle.hint = value + } + +class TextItemConfig : IDslItemConfig { + + /**[R.id.lib_text_view]*/ + var itemTextViewId: Int = R.id.lib_text_view + + /**条目文本*/ + var itemText: CharSequence? = null + set(value) { + field = value + itemTextStyle.text = value + } + + /**统一样式配置*/ + var itemTextStyle: TextStyleConfig = TextStyleConfig() + +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ImageStyleConfig.kt b/dslitem/src/main/java/com/angcyo/item/style/ImageStyleConfig.kt new file mode 100644 index 0000000..7f8e9eb --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ImageStyleConfig.kt @@ -0,0 +1,30 @@ +package com.angcyo.item.style + +import android.view.View +import android.widget.ImageView + +/** + * 图片控件样式 + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +class ImageStyleConfig : ViewStyleConfig() { + + var imageScaleType: ImageView.ScaleType? = null + + override fun updateStyle(view: View) { + super.updateStyle(view) + + if (view is ImageView) { + // + if (imageScaleType == null) { + imageScaleType = view.scaleType + } + view.scaleType = imageScaleType + + // + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/TextStyleConfig.kt b/dslitem/src/main/java/com/angcyo/item/style/TextStyleConfig.kt new file mode 100644 index 0000000..4bb1f52 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/TextStyleConfig.kt @@ -0,0 +1,145 @@ +package com.angcyo.item.style + +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.TextView +import com.angcyo.dsladapter.UndefinedDrawable +import com.angcyo.dsladapter.undefined_size +import com.angcyo.dsladapter.visible +import com.angcyo.item.bottomIco +import com.angcyo.item.leftIco +import com.angcyo.item.rightIco +import com.angcyo.item.setBoldText +import com.angcyo.item.topIco +import com.angcyo.widget.span.undefined_color +import com.angcyo.widget.span.undefined_float + +/** + * 文本样式配置 + * Email:angcyo@126.com + * @author angcyo + * @date 2020/06/09 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ + +open class TextStyleConfig : ViewStyleConfig() { + + /**显示的文本内容*/ + var text: CharSequence? = null + + /**当[text]为null时, 隐藏控件*/ + var goneOnTextEmpty: Boolean = false + + /**提示文本内容*/ + var hint: CharSequence? = null + var textBold: Boolean = false + var textColor: Int = undefined_color + var textColors: ColorStateList? = null + + var textSize: Float = undefined_float + var textGravity: Int = Gravity.LEFT or Gravity.CENTER_VERTICAL + + /**四向图标, 需要指定bounds*/ + var leftDrawable: Drawable? = UndefinedDrawable() + var topDrawable: Drawable? = UndefinedDrawable() + var rightDrawable: Drawable? = UndefinedDrawable() + var bottomDrawable: Drawable? = UndefinedDrawable() + var drawablePadding = undefined_size + + /**生效*/ + override fun updateStyle(view: View) { + super.updateStyle(view) + + if (view is TextView) { + + if (text == null) { + text = view.text + } + + if (hint == null) { + hint = view.hint + } + + with(view) { + text = this@TextStyleConfig.text + + //[TextView]设置最小宽度和最小高度 + if (viewMinWidth != undefined_size) { + minWidth = viewMinWidth + + } + if (viewMinHeight != undefined_size) { + minHeight = viewMinHeight + } + + if (goneOnTextEmpty) { + visible(!this@TextStyleConfig.text.isNullOrEmpty()) + } + + //兼容 + view.hint = hint + + gravity = textGravity + + setBoldText(textBold) + + //颜色, 防止复用. 所以在未指定的情况下, 要获取默认的颜色. + val colors = when { + this@TextStyleConfig.textColors != null -> { + this@TextStyleConfig.textColors + } + + textColor != undefined_color -> { + ColorStateList.valueOf(textColor) + } + + else -> { + textColors + } + } + if (colors != this@TextStyleConfig.textColors) { + this@TextStyleConfig.textColors = colors + } + setTextColor(colors) + + //字体大小同理. + val size = if (this@TextStyleConfig.textSize != undefined_float) { + this@TextStyleConfig.textSize + } else { + textSize + } + this@TextStyleConfig.textSize = size + setTextSize(TypedValue.COMPLEX_UNIT_PX, size) + + //padding + if (drawablePadding == undefined_size) { + drawablePadding = compoundDrawablePadding + } + compoundDrawablePadding = drawablePadding + + //四向图标修改 + if (leftDrawable is UndefinedDrawable) { + leftDrawable = leftIco() + } + if (topDrawable is UndefinedDrawable) { + topDrawable = topIco() + } + if (rightDrawable is UndefinedDrawable) { + rightDrawable = rightIco() + } + if (bottomDrawable is UndefinedDrawable) { + bottomDrawable = bottomIco() + } + setCompoundDrawablesRelative( + leftDrawable, + topDrawable, + rightDrawable, + bottomDrawable + ) + } + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/item/style/ViewStyleConfig.kt b/dslitem/src/main/java/com/angcyo/item/style/ViewStyleConfig.kt new file mode 100644 index 0000000..363ca00 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/item/style/ViewStyleConfig.kt @@ -0,0 +1,143 @@ +package com.angcyo.item.style + +import android.graphics.drawable.Drawable +import android.os.Build +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import com.angcyo.dsladapter.UndefinedDrawable +import com.angcyo.dsladapter.setBgDrawable +import com.angcyo.dsladapter.setWidthHeight +import com.angcyo.dsladapter.undefined_size +import com.angcyo.widget.span.undefined_int + +/** + * View基础样式配置 + * Email:angcyo@126.com + * @author angcyo + * @date 2020/06/09 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ + +open class ViewStyleConfig { + + /**限制最小宽高*/ + var viewWidth: Int = undefined_size + var viewMinWidth: Int = undefined_size + + /**指定宽高*/ + var viewHeight: Int = undefined_size + var viewMinHeight: Int = undefined_size + + /**视图可见性[visibility]*/ + var viewVisibility: Int = undefined_int + + /**padding值*/ + var paddingLeft: Int = undefined_size + var paddingRight: Int = undefined_size + var paddingTop: Int = undefined_size + var paddingBottom: Int = undefined_size + + /**背景*/ + var backgroundDrawable: Drawable? = UndefinedDrawable() + + /**部分布局支持*/ + var layoutGravity: Int = Gravity.NO_GRAVITY + + /** + * 需要parent为[ConstraintLayout] + * [androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.dimensionRatio]*/ + var viewDimensionRatio: String? = null + + /**更新样式*/ + open fun updateStyle(view: View) { + with(view) { + //初始化默认值 + if (this@ViewStyleConfig.paddingLeft == undefined_size) { + this@ViewStyleConfig.paddingLeft = paddingLeft + } + if (this@ViewStyleConfig.paddingRight == undefined_size) { + this@ViewStyleConfig.paddingRight = paddingRight + } + if (this@ViewStyleConfig.paddingTop == undefined_size) { + this@ViewStyleConfig.paddingTop = paddingTop + } + if (this@ViewStyleConfig.paddingBottom == undefined_size) { + this@ViewStyleConfig.paddingBottom = paddingBottom + } + + //可见性 + if (viewVisibility == undefined_int) { + viewVisibility = visibility + } + visibility = viewVisibility + + //设置 + setPadding( + this@ViewStyleConfig.paddingLeft, + this@ViewStyleConfig.paddingTop, + this@ViewStyleConfig.paddingRight, + this@ViewStyleConfig.paddingBottom + ) + + val lp = layoutParams + + if (backgroundDrawable is UndefinedDrawable) { + backgroundDrawable = background + } + setBgDrawable(backgroundDrawable) + + //初始化默认值 + if (viewMinWidth == undefined_size && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + viewMinWidth = minimumWidth + } + if (viewMinHeight == undefined_size && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + viewMinHeight = minimumHeight + } + //设置 + if (viewMinWidth != undefined_size) { + minimumWidth = viewMinWidth + when (view) { + is ConstraintLayout -> view.minWidth = viewMinWidth + } + } + if (viewMinHeight != undefined_size) { + minimumHeight = viewMinHeight + when (view) { + is ConstraintLayout -> view.minHeight = viewMinHeight + } + } + + //初始化默认值 + if (viewWidth == undefined_size) { + viewWidth = lp.width + } + if (viewHeight == undefined_size) { + viewHeight = lp.height + } + //设置 + setWidthHeight(viewWidth, viewHeight) + if (lp is ConstraintLayout.LayoutParams) { + if (viewDimensionRatio == null) { + viewDimensionRatio = lp.dimensionRatio + } else { + lp.dimensionRatio = viewDimensionRatio + layoutParams = lp + } + } + + //Gravity + if (lp is FrameLayout.LayoutParams) { + val oldGravity = lp.gravity + if (layoutGravity == Gravity.NO_GRAVITY) { + layoutGravity = oldGravity + } + lp.gravity = layoutGravity + if (oldGravity != layoutGravity) { + layoutParams = lp + } + } + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/BadgeTextView.kt b/dslitem/src/main/java/com/angcyo/widget/BadgeTextView.kt new file mode 100644 index 0000000..8c00155 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/BadgeTextView.kt @@ -0,0 +1,100 @@ +package com.angcyo.widget + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import com.angcyo.drawable.DslAttrBadgeDrawable +import com.angcyo.item.isNotSpecified + +/** + * 单纯的用来绘制角标的控件 + * 使用属性[app:r_badge_text="xxx"]设置角标 + * + * badgePaddingLeft + * badgePaddingRight + * badgePaddingTop + * badgePaddingBottom + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +class BadgeTextView : AppCompatTextView, IBadgeView { + + /**角标绘制*/ + override var dslBadeDrawable = DslAttrBadgeDrawable() + + constructor(context: Context) : super(context) { + initAttribute(context, null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initAttribute(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + initAttribute(context, attrs) + } + + private fun initAttribute(context: Context, attributeSet: AttributeSet?) { + dslBadeDrawable.apply { + badgeOffsetX = 0 + badgeOffsetY = 0 + initAttribute(context, attributeSet) + callback = this@BadgeTextView + dslGravity.gravityRelativeCenter = false + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + dslBadeDrawable.apply { + setBounds(0, 0, measuredWidth, measuredHeight) + draw(canvas) + } + } + + /*override fun setText(text: CharSequence?, type: BufferType?) { + super.setText(text, type) + //在构造方法中调用此方法[dslBadeDrawable]为空 + dslBadeDrawable?.badgeText = text.toString() + }*/ + + /**角标的文本, 空字符串会绘制成小圆点*/ + fun updateBadge(text: String? = null, action: DslAttrBadgeDrawable.() -> Unit = {}) { + dslBadeDrawable.apply { + drawBadge = true + //badgeGravity = Gravity.TOP or Gravity.RIGHT + badgeText = text + //badgeCircleRadius + //badgeOffsetY = 4 * dpi + //cornerRadius(25 * dp) + action() + } + postInvalidateOnAnimation() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + //of kotlin + //val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + //val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + + if (widthMode.isNotSpecified() && heightMode.isNotSpecified()) { + setMeasuredDimension( + dslBadeDrawable.intrinsicWidth + paddingLeft + paddingRight, + dslBadeDrawable.intrinsicHeight + paddingTop + paddingBottom + ) + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/CharLengthFilter.kt b/dslitem/src/main/java/com/angcyo/widget/CharLengthFilter.kt new file mode 100644 index 0000000..45d5e94 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/CharLengthFilter.kt @@ -0,0 +1,69 @@ +package com.angcyo.widget + +import android.text.InputFilter +import android.text.Spanned + +/** + * 使用英文字符数过滤, 一个汉字等于2个英文, 一个emoji表情等于2个汉字 + * Created by angcyo on 2018-08-10. + * Email:angcyo@126.com + */ +class CharLengthFilter(var maxLen: Int) : InputFilter { + + companion object { + const val MAX_CHAR: Char = 255.toChar() + } + + /** + * 将 dest 字符中, 的dstart 位置到 dend 位置的字符串, + * 替换成 source 字符中, 的start 位置到 end 对应的字符串 + */ + override fun filter( + source: CharSequence, + start: Int, + end: Int, + dest: Spanned, + dstart: Int, + dend: Int + ): CharSequence? { + var dIndex = 0 + var count = 0 + var dCount = 0 //dest 中, 需要替换掉多少个char + //当前已经存在char数量 + while (count <= maxLen && dIndex < dest.length) { + val c = dest[dIndex++] + if (c <= MAX_CHAR) { + count += 1 + if (dIndex in dstart until dend) { + dCount += 1 + } + } else { + if (dIndex in dstart until dend) { + dCount += 1 + } + count += 2 + } + } + count -= dCount + if (count > maxLen) { + return dest.subSequence(0, dIndex - 1) + } + //本次需要输入的char数量 + var sIndex = 0 + while (count <= maxLen && sIndex < source.length) { + val c = source[sIndex++] + count = if (c <= MAX_CHAR) { + count + 1 + } else { + count + 2 + } + } + return if (count > maxLen) { //已经存在的char长度, + 输入的char长度, 大于限制长度 + //越界 + sIndex-- + source.subSequence(0, sIndex) + } else { // keep original + null + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/DslGravity.kt b/dslitem/src/main/java/com/angcyo/widget/DslGravity.kt new file mode 100644 index 0000000..8d4d762 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/DslGravity.kt @@ -0,0 +1,242 @@ +package com.angcyo.widget + +import android.graphics.Rect +import android.graphics.RectF +import android.view.Gravity +import androidx.core.view.GravityCompat + +/** + * [android.view.Gravity] 辅助计算类 + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/13 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +class DslGravity { + + /**束缚范围*/ + val gravityBounds: RectF = RectF() + + /**束缚重力*/ + var gravity: Int = Gravity.LEFT or Gravity.TOP + + /**使用中心坐标作为定位参考, 否则就是四条边 + * 就是将目标的中点放在[gravityBounds]的[gravity]位置*/ + var gravityRelativeCenter: Boolean = true + + /**额外偏移距离, 会根据[Gravity]自动取负值*/ + var gravityOffsetX: Float = 0f + var gravityOffsetY: Float = 0f + + fun setGravityBounds(rectF: RectF) { + gravityBounds.set(rectF) + } + + fun setGravityBounds(rect: Rect) { + gravityBounds.set(rect) + } + + fun setGravityBounds( + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + gravityBounds.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) + } + + fun setGravityBounds( + left: Float, + top: Float, + right: Float, + bottom: Float + ) { + gravityBounds.set(left, top, right, bottom) + } + + //计算后的属性 + var _horizontalGravity: Int = Gravity.LEFT + var _verticalGravity: Int = Gravity.TOP + var _isCenterGravity: Boolean = false + var _targetWidth = 0f + var _targetHeight = 0f + + /**计算后的属性, 可以直接读取使用*/ + var _gravityLeft = 0f + var _gravityTop = 0f + var _gravityRight = 0f + var _gravityBottom = 0f + + //根据gravity调整后的offset + var _gravityOffsetX = 0f + var _gravityOffsetY = 0f + + /**根据[gravity]返回在[gravityBounds]中的[left] [top]位置*/ + fun applyGravity( + width: Float = _targetWidth, + height: Float = _targetHeight, + callback: (centerX: Float, centerY: Float) -> Unit + ) { + + _targetWidth = width + _targetHeight = height + + val layoutDirection = 0 + val absoluteGravity = GravityCompat.getAbsoluteGravity(gravity, layoutDirection) + val verticalGravity = gravity and Gravity.VERTICAL_GRAVITY_MASK + val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK + + //调整offset + _gravityOffsetX = when (horizontalGravity) { + Gravity.RIGHT -> -gravityOffsetX + Gravity.END -> -gravityOffsetX + else -> gravityOffsetX + } + _gravityOffsetY = when (verticalGravity) { + Gravity.BOTTOM -> -gravityOffsetY + else -> gravityOffsetY + } + + //计算居中的坐标 + val centerX = when (horizontalGravity) { + Gravity.CENTER_HORIZONTAL -> gravityBounds.left + gravityBounds.width() / 2 + _gravityOffsetX + Gravity.RIGHT -> gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2 + Gravity.END -> gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2 + else -> gravityBounds.left + _gravityOffsetX + if (gravityRelativeCenter) 0f else _targetWidth / 2 + } + + val centerY = when (verticalGravity) { + Gravity.CENTER_VERTICAL -> gravityBounds.top + gravityBounds.height() / 2 + _gravityOffsetY + Gravity.BOTTOM -> gravityBounds.bottom + _gravityOffsetY - if (gravityRelativeCenter) 0f else _targetHeight / 2 + else -> gravityBounds.top + _gravityOffsetY + if (gravityRelativeCenter) 0f else _targetHeight / 2 + } + + //缓存 + _horizontalGravity = horizontalGravity + _verticalGravity = verticalGravity + _isCenterGravity = horizontalGravity == Gravity.CENTER_HORIZONTAL && + verticalGravity == Gravity.CENTER_VERTICAL + + _gravityLeft = centerX - _targetWidth / 2 + _gravityRight = centerX + _targetWidth / 2 + _gravityTop = centerY - _targetHeight / 2 + _gravityBottom = centerY + _targetHeight / 2 + + //回调 + callback(centerX, centerY) + } +} + +/** + * 默认计算出的是目标中心点坐标参考距离 + * [com.angcyo.drawable.DslGravity.getGravityRelativeCenter] + * */ +fun dslGravity( + rect: RectF, //计算的矩形 + gravity: Int, //重力 + width: Float, //放置目标的宽度 + height: Float, //放置目标的高度 + offsetX: Float = 0f, //额外的偏移,会根据[gravity]进行左右|上下取反 + offsetY: Float = 0f, + gravityRelativeCenter: Boolean = true, + callback: (dslGravity: DslGravity, centerX: Float, centerY: Float) -> Unit +): DslGravity { + val _dslGravity = DslGravity() + _dslGravity.setGravityBounds(rect) + _config(_dslGravity, gravity, width, height, offsetX, offsetY, gravityRelativeCenter, callback) + return _dslGravity +} + +/** + * 默认计算出的是目标中心点坐标参考距离 + * [com.angcyo.drawable.DslGravity.getGravityRelativeCenter] + * */ +fun dslGravity( + rect: Rect, //计算的矩形 + gravity: Int, //重力 + width: Float, //放置目标的宽度 + height: Float, //放置目标的高度 + offsetX: Float = 0f, //额外的偏移,会根据[gravity]进行左右|上下取反 + offsetY: Float = 0f, + gravityRelativeCenter: Boolean = true, + callback: (dslGravity: DslGravity, centerX: Float, centerY: Float) -> Unit +): DslGravity { + val _dslGravity = DslGravity() + _dslGravity.setGravityBounds(rect) + _config(_dslGravity, gravity, width, height, offsetX, offsetY, gravityRelativeCenter, callback) + return _dslGravity +} + +private fun _config( + _dslGravity: DslGravity, + gravity: Int, //重力 + width: Float, //放置目标的宽度 + height: Float, //放置目标的高度 + offsetX: Float = 0f, //额外的偏移,会根据[gravity]进行左右|上下取反 + offsetY: Float = 0f, + gravityRelativeCenter: Boolean = true, + callback: (dslGravity: DslGravity, centerX: Float, centerY: Float) -> Unit +) { + _dslGravity.gravity = gravity + _dslGravity.gravityOffsetX = offsetX + _dslGravity.gravityOffsetY = offsetY + _dslGravity.gravityRelativeCenter = gravityRelativeCenter + _dslGravity.applyGravity(width, height) { centerX, centerY -> + callback(_dslGravity, centerX, centerY) + } +} + +/**Gravity居中*/ +fun Int.isGravityCenter(): Boolean { + val layoutDirection = 0 + val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection) + val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK + val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK + + return verticalGravity == Gravity.CENTER_VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL +} + +/**Gravity水平居中*/ +fun Int.isGravityCenterHorizontal(): Boolean { + val layoutDirection = 0 + val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection) + val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK + + return horizontalGravity == Gravity.CENTER_HORIZONTAL +} + +/**Gravity垂直居中*/ +fun Int.isGravityCenterVertical(): Boolean { + val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK + return verticalGravity == Gravity.CENTER_VERTICAL +} + +/**Gravity左*/ +fun Int.isGravityLeft(): Boolean { + val layoutDirection = 0 + val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection) + val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK + + return horizontalGravity == Gravity.LEFT +} + +/**Gravity右*/ +fun Int.isGravityRight(): Boolean { + val layoutDirection = 0 + val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection) + val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK + + return horizontalGravity == Gravity.RIGHT +} + +/**Gravity上*/ +fun Int.isGravityTop(): Boolean { + val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK + return verticalGravity == Gravity.TOP +} + +/**Gravity下*/ +fun Int.isGravityBottom(): Boolean { + val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK + return verticalGravity == Gravity.BOTTOM +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/DslSelector.kt b/dslitem/src/main/java/com/angcyo/widget/DslSelector.kt new file mode 100644 index 0000000..efcdb66 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/DslSelector.kt @@ -0,0 +1,437 @@ +package com.angcyo.widget + +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import com.angcyo.item.isChange + +/** + * 用来操作[ViewGroup]中的[child], 支持单选, 多选, 拦截. + * 操作的都是可见性为[VISIBLE]的[View] + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/11/24 + */ + +open class DslSelector { + + var parent: ViewGroup? = null + var dslSelectorConfig: DslSelectorConfig = DslSelectorConfig() + + //可见view列表 + val visibleViewList: MutableList = mutableListOf() + + /** + * 选中的索引列表 + * */ + val selectorIndexList: MutableList = mutableListOf() + get() { + field.clear() + visibleViewList.forEachIndexed { index, view -> + if (view.isSe()) { + field.add(index) + } + } + + return field + } + + /** + * 选中的View列表 + * */ + val selectorViewList: MutableList = mutableListOf() + get() { + field.clear() + visibleViewList.forEachIndexed { index, view -> + if (view.isSe() || index == dslSelectIndex) { + field.add(view) + } + } + return field + } + + //child 点击事件处理 + val _onChildClickListener = View.OnClickListener { + val index = visibleViewList.indexOf(it) + val select = + if (dslSelectorConfig.dslMultiMode || dslSelectorConfig.dslMinSelectLimit < 1) { + !it.isSe() + } else { + true + } + + if (!interceptSelector(index, select, true)) { + selector( + visibleViewList.indexOf(it), + select, + notify = true, + fromUser = true, + forceNotify = it is CompoundButton && dslSelectorConfig.dslMultiMode + ) + } + } + + /**兼容[CompoundButton]*/ + val _onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { buttonView, isChecked -> + buttonView.isChecked = buttonView.isSelected //恢复状态 不做任何处理, 在[OnClickListener]中处理 + /*val index = visibleViewList.indexOf(buttonView) + + if (interceptSelector(index, isChecked, false)) { + //拦截了此操作 + buttonView.isChecked = !isChecked //恢复状态 + } + + val selectorViewList = selectorViewList + val sum = selectorViewList.size + //Limit 过滤 + if (buttonView.isChecked) { + if (sum > dslSelectorConfig.dslMaxSelectLimit) { + //不允许选择 + buttonView.isChecked = false //恢复状态 + } + } else { + //取消选择, 检查是否达到了 limit + if (sum < dslSelectorConfig.dslMinSelectLimit) { + //不允许取消选择 + buttonView.isChecked = true //恢复状态 + } + } + + if (isChecked) { + //已经选中了控件 + } else { + //已经取消了控件 + }*/ + } + + /**当前选中的索引*/ + var dslSelectIndex = -1 + + /**安装*/ + fun install(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector { + dslSelectIndex = -1 + parent = viewGroup + updateVisibleList() + dslSelectorConfig.config() + + updateStyle() + updateClickListener() + + if (dslSelectIndex in 0 until visibleViewList.size) { + selector(dslSelectIndex) + } + + return this + } + + /**更新样式*/ + fun updateStyle() { + visibleViewList.forEachIndexed { index, view -> + val selector = dslSelectIndex == index || view.isSe() + dslSelectorConfig.onStyleItemView(view, index, selector) + } + } + + /**更新child的点击事件*/ + fun updateClickListener() { + parent?.apply { + for (i in 0 until childCount) { + getChildAt(i)?.let { + it.setOnClickListener(_onChildClickListener) + if (it is CompoundButton) { + it.setOnCheckedChangeListener(_onCheckedChangeListener) + } + } + } + } + } + + /**更新可见视图列表*/ + fun updateVisibleList(): List { + visibleViewList.clear() + parent?.apply { + for (i in 0 until childCount) { + getChildAt(i)?.let { + if (it.visibility == View.VISIBLE) { + visibleViewList.add(it) + } + } + } + } + if (dslSelectIndex in visibleViewList.indices) { + if (!visibleViewList[dslSelectIndex].isSe()) { + visibleViewList[dslSelectIndex].setSe(true) + } + } else { + //如果当前选中的索引, 不在可见视图列表中 + dslSelectIndex = -1 + } + return visibleViewList + } + + /** + * 操作单个 + * @param index 操作目标的索引值 + * @param select 选中 or 取消选中 + * @param notify 是否需要通知事件 + * @param forceNotify 是否强制通知事件.child使用[CompoundButton]时, 推荐使用 + * */ + fun selector( + index: Int, + select: Boolean = true, + notify: Boolean = true, + fromUser: Boolean = false, + forceNotify: Boolean = false + ) { + val oldSelectorList = selectorIndexList.toList() + val lastSelectorIndex: Int? = oldSelectorList.lastOrNull() + val reselect = !dslSelectorConfig.dslMultiMode && + oldSelectorList.isNotEmpty() && + oldSelectorList.contains(index) + + var needNotify = _selector(index, select, fromUser) || forceNotify + + if (!oldSelectorList.isChange(selectorIndexList)) { + //选中项, 未改变时不通知 + needNotify = false + } + + if (needNotify || reselect) { + dslSelectIndex = selectorIndexList.lastOrNull() ?: -1 + if (notify) { + notifySelectChange(lastSelectorIndex ?: -1, reselect, fromUser) + } + } + } + + /**选择所有 + * [select] true:选择所有, false:取消所有*/ + fun selectorAll( + select: Boolean = true, + notify: Boolean = true, + fromUser: Boolean = true + ) { + val indexList = visibleViewList.mapIndexedTo(mutableListOf()) { index, _ -> + index + } + selector(indexList, select, notify, fromUser) + } + + /** + * 操作多个 + * @param select 选中 or 取消选中 + * [selector] + * */ + fun selector( + indexList: MutableList, + select: Boolean = true, + notify: Boolean = true, + fromUser: Boolean = false + ) { + val oldSelectorIndexList = selectorIndexList + val lastSelectorIndex: Int? = oldSelectorIndexList.lastOrNull() + + var result = false + + indexList.forEach { + result = _selector(it, select, fromUser) || result + } + + if (result) { + dslSelectIndex = selectorIndexList.lastOrNull() ?: -1 + if (notify) { + notifySelectChange(lastSelectorIndex ?: -1, false, fromUser) + } + } + } + + /**通知事件*/ + fun notifySelectChange(lastSelectorIndex: Int, reselect: Boolean, fromUser: Boolean) { + val indexSelectorList = selectorIndexList + dslSelectorConfig.onSelectViewChange( + visibleViewList.getOrNull(lastSelectorIndex), + selectorViewList, + reselect, + fromUser + ) + dslSelectorConfig.onSelectIndexChange( + lastSelectorIndex, + indexSelectorList, + reselect, + fromUser + ) + } + + /**当前的操作是否被拦截*/ + fun interceptSelector(index: Int, select: Boolean, fromUser: Boolean): Boolean { + val visibleViewList = visibleViewList + if (index !in 0 until visibleViewList.size) { + return true + } + return dslSelectorConfig.onSelectItemView(visibleViewList[index], index, select, fromUser) + } + + /**@return 是否发生过改变*/ + fun _selector(index: Int, select: Boolean, fromUser: Boolean): Boolean { + val visibleViewList = visibleViewList + //超范围过滤 + if (index !in 0 until visibleViewList.size) { + return false + } + + val selectorIndexList = selectorIndexList + val selectorViewList = selectorViewList + + if (selectorIndexList.isNotEmpty()) { + if (select) { + //需要选中某项 + + if (dslSelectorConfig.dslMultiMode) { + //多选模式 + if (selectorIndexList.contains(index)) { + //已经选中 + return false + } + } else { + //单选模式 + + //取消之前选中 + selectorIndexList.forEach { + if (it != index) { + visibleViewList[it].setSe(false) + } + } + + if (selectorIndexList.contains(index)) { + //已经选中 + return true + } + } + + } else { + //需要取消选中 + if (!selectorIndexList.contains(index)) { + //目标已经是未选中 + return false + } + } + } + + //Limit 过滤 + if (select) { + val sum = selectorViewList.size + 1 + if (sum > dslSelectorConfig.dslMaxSelectLimit) { + //不允许选择 + return false + } + } else { + //取消选择, 检查是否达到了 limit + val sum = selectorViewList.size - 1 + if (sum < dslSelectorConfig.dslMinSelectLimit) { + //不允许取消选择 + return false + } + } + + val selectorView = visibleViewList[index] + + //更新选中样式 + selectorView.setSe(select) + + if (dslSelectorConfig.dslMultiMode) { + //多选 + } else { + //单选 + + //取消之前 + selectorViewList.forEach { view -> + //更新样式 + val indexOf = visibleViewList.indexOf(view) + if (indexOf != index && + !dslSelectorConfig.onSelectItemView(view, indexOf, false, fromUser) + ) { + view.setSe(false) + dslSelectorConfig.onStyleItemView(view, indexOf, false) + } + } + } + + dslSelectorConfig.onStyleItemView(selectorView, index, select) + + return true + } + + /**是否选中状态*/ + fun View.isSe(): Boolean { + return isSelected || if (this is CompoundButton) isChecked else false + } + + fun View.setSe(se: Boolean) { + isSelected = se + if (this is CompoundButton) isChecked = se + } +} + +/** + * Dsl配置项 + * */ +open class DslSelectorConfig { + + /**取消选择时, 最小需要保持多个选中. 可以决定单选时, 是否允许取消所有选中*/ + var dslMinSelectLimit = 1 + + /**多选时, 最大允许多个选中*/ + var dslMaxSelectLimit = Int.MAX_VALUE + + /**是否是多选模式*/ + var dslMultiMode: Boolean = false + + /** + * 用来初始化[itemView]的样式 + * [onSelectItemView] + * */ + var onStyleItemView: (itemView: View, index: Int, select: Boolean) -> Unit = + { _, _, _ -> + + } + + /** + * 选中[View]改变回调, 优先于[onSelectIndexChange]触发, 区别在于参数类型不一样 + * @param fromView 单选模式下有效, 表示之前选中的[View] + * @param reselect 是否是重复选择, 只在单选模式下有效 + * @param fromUser 是否是用户产生的回调, 而非代码设置 + * */ + var onSelectViewChange: (fromView: View?, selectViewList: List, reselect: Boolean, fromUser: Boolean) -> Unit = + { _, _, _, _ -> + + } + + /** + * 选中改变回调 + * [onSelectViewChange] + * @param fromIndex 单选模式下有效, 表示之前选中的[View], 在可见性[child]列表中的索引 + * */ + var onSelectIndexChange: (fromIndex: Int, selectIndexList: List, reselect: Boolean, fromUser: Boolean) -> Unit = + { fromIndex, selectList, reselect, fromUser -> + } + + /** + * 当需要选中[itemView]时回调, 返回[true]表示拦截默认处理 + * @param itemView 操作的[View] + * @param index [itemView]在可见性view列表中的索引. 非ViewGroup中的索引 + * @param select 选中 or 取消选中 + * @return true 表示拦截默认处理 + * */ + var onSelectItemView: (itemView: View, index: Int, select: Boolean, fromUser: Boolean) -> Boolean = + { _, _, _, _ -> + false + } +} + +/**[DslSelector]组件*/ +fun dslSelector(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector { + return DslSelector().apply { + install(viewGroup, config) + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/IBadgeView.kt b/dslitem/src/main/java/com/angcyo/widget/IBadgeView.kt new file mode 100644 index 0000000..9e45419 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/IBadgeView.kt @@ -0,0 +1,14 @@ +package com.angcyo.widget + +import com.angcyo.drawable.DslAttrBadgeDrawable + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/09/23 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface IBadgeView { + var dslBadeDrawable: DslAttrBadgeDrawable +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/ITouchDelegate.kt b/dslitem/src/main/java/com/angcyo/widget/ITouchDelegate.kt new file mode 100644 index 0000000..1beca0f --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/ITouchDelegate.kt @@ -0,0 +1,44 @@ +package com.angcyo.widget + +import android.view.MotionEvent + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/11/19 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface ITouchDelegate { + fun getTouchActionDelegate(): TouchActionDelegate +} + +fun ITouchDelegate.onDispatchTouchEventAction(action: (ev: MotionEvent) -> Unit): TouchListener { + val listener = object : TouchListener { + override fun onDispatchTouchEventAction(ev: MotionEvent) { + action(ev) + } + } + getTouchActionDelegate()?.touchListener?.add(listener) + return listener +} + +fun ITouchDelegate.onInterceptTouchEventAction(action: (ev: MotionEvent) -> Unit): TouchListener { + val listener = object : TouchListener { + override fun onInterceptTouchEventAction(ev: MotionEvent) { + action(ev) + } + } + getTouchActionDelegate()?.touchListener?.add(listener) + return listener +} + +fun ITouchDelegate.onTouchEventAction(action: (ev: MotionEvent) -> Unit): TouchListener { + val listener = object : TouchListener { + override fun onTouchEventAction(ev: MotionEvent) { + action(ev) + } + } + getTouchActionDelegate()?.touchListener?.add(listener) + return listener +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/ITouchHold.kt b/dslitem/src/main/java/com/angcyo/widget/ITouchHold.kt new file mode 100644 index 0000000..61110b4 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/ITouchHold.kt @@ -0,0 +1,13 @@ +package com.angcyo.widget + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2021/06/28 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface ITouchHold { + /**手势是否为松开*/ + var isTouchHold: Boolean +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/TouchActionDelegate.kt b/dslitem/src/main/java/com/angcyo/widget/TouchActionDelegate.kt new file mode 100644 index 0000000..3b29dc1 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/TouchActionDelegate.kt @@ -0,0 +1,34 @@ +package com.angcyo.widget + +import android.view.MotionEvent + +/** + * 手势代理 + * Email:angcyo@126.com + * @author angcyo + * @date 2021/11/19 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +open class TouchActionDelegate { + + val touchListener = mutableSetOf() + + fun dispatchTouchEvent(ev: MotionEvent) { + touchListener.forEach { + it.onDispatchTouchEventAction(ev) + } + } + + fun onInterceptTouchEvent(ev: MotionEvent) { + touchListener.forEach { + it.onInterceptTouchEventAction(ev) + } + } + + fun onTouchEvent(ev: MotionEvent) { + touchListener.forEach { + it.onTouchEventAction(ev) + } + } + +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/TouchListener.kt b/dslitem/src/main/java/com/angcyo/widget/TouchListener.kt new file mode 100644 index 0000000..8d097dc --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/TouchListener.kt @@ -0,0 +1,25 @@ +package com.angcyo.widget + +import android.view.MotionEvent + +/** + * Touch监听 + * Email:angcyo@126.com + * @author angcyo + * @date 2021/11/19 + * Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved. + */ +interface TouchListener { + + fun onDispatchTouchEventAction(ev: MotionEvent) { + + } + + fun onInterceptTouchEventAction(ev: MotionEvent) { + + } + + fun onTouchEventAction(ev: MotionEvent) { + + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/ViewGroupEx.kt b/dslitem/src/main/java/com/angcyo/widget/ViewGroupEx.kt new file mode 100644 index 0000000..11ca7ae --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/ViewGroupEx.kt @@ -0,0 +1,83 @@ +package com.angcyo.widget + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.IdRes +import com.angcyo.dsladapter.size +import com.angcyo.item.R +import kotlin.math.absoluteValue +import kotlin.math.min + +/** + * Kotlin ViewGroup的扩展 + * Created by angcyo on 2017-07-26. + */ + +fun View?.find(@IdRes id: Int): T? { + return this?.findViewById(id) +} + +fun ViewGroup.resetChild( + list: List?, + layoutId: Int, + init: (itemView: View, item: T, itemIndex: Int) -> Unit = { _, _, _ -> } +) { + resetChild(list.size(), layoutId) { itemView, itemIndex -> + val item = list!!.get(itemIndex) + init(itemView, item, itemIndex) + } +} + +fun ViewGroup.resetChild( + size: Int, + layoutId: Int, + init: (itemView: View, itemIndex: Int) -> Unit = { _, _ -> } +) { + //如果布局id不一样, 说明child不一样, 需要remove + for (index in childCount - 1 downTo 0) { + val tag = getChildAt(index).getTag(R.id.tag) + if (tag == null || (tag is Int && tag != layoutId)) { + removeViewAt(index) + } + } + + resetChildCount(size) { childIndex, childView -> + if (childView == null) { + val itemView = LayoutInflater.from(context).inflate(layoutId, this, false) + itemView.setTag(R.id.tag, layoutId) + itemView + } else { + childView + } + } + + for (i in 0 until size) { + init(getChildAt(i), i) + } +} + +/**将子View的数量, 重置到指定的数量*/ +fun ViewGroup.resetChildCount( + newSize: Int, + createOrInitView: (childIndex: Int, childView: View?) -> View +) { + val oldSize = childCount + val count = newSize - oldSize + if (count > 0) { + //需要补充子View + for (i in 0 until count) { + addView(createOrInitView.invoke(oldSize + i, null)) + } + } else if (count < 0) { + //需要移除子View + for (i in 0 until count.absoluteValue) { + removeViewAt(oldSize - 1 - i) + } + } + + //初始化 + for (i in 0 until min(oldSize, newSize)) { + createOrInitView.invoke(i, getChildAt(i)) + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/DslRecyclerView.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/DslRecyclerView.kt new file mode 100644 index 0000000..3d2b31e --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/DslRecyclerView.kt @@ -0,0 +1,353 @@ +package com.angcyo.widget.recycler + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.RecyclerView +import com.angcyo.item.R +import com.angcyo.item.isTouchDown +import com.angcyo.item.isTouchFinish +import com.angcyo.widget.ITouchDelegate +import com.angcyo.widget.ITouchHold +import com.angcyo.widget.TouchActionDelegate +import java.lang.ref.WeakReference +import java.util.* + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/23 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ + +typealias FocusTransitionChanged = (from: View?, to: View?) -> Unit + +open class DslRecyclerView : RecyclerView, ITouchHold, ITouchDelegate { + + companion object { + //用于在多个RV之间共享之前的焦点View + var _lastFocusViewRef: WeakReference? = null + + /**焦点过渡改变回调, 如果在此回调中处理了视图的属性, 要注意RecyclerView中的复用问题*/ + var _focusTransitionChangedList: MutableList = mutableListOf() + + fun addFocusTransitionChangeListener(listener: FocusTransitionChanged) { + if (!_focusTransitionChangedList.contains(listener)) { + _focusTransitionChangedList.add(listener) + } + } + + fun removeFocusTransitionChangeListener(listener: FocusTransitionChanged) { + if (_focusTransitionChangedList.contains(listener)) { + _focusTransitionChangedList.remove(listener) + } + } + + /**全局统一设置焦点视图*/ + fun setFocusView(parent: View, view: View?) { + val old = _lastFocusViewRef?.get() + if (old != view) { + _lastFocusViewRef?.clear() + _lastFocusViewRef = null + if (view != null) { + _lastFocusViewRef = WeakReference(view) + } + _focusTransitionChangedList.forEach { + it.invoke(old, view) + } + ViewCompat.postInvalidateOnAnimation(parent) + } + } + } + + val _touchDelegate = TouchActionDelegate() + + /** 通过[V] [H] [GV2] [GH3] [SV2] [SV3] 方式, 设置 [LayoutManager] */ + var layout: String? = null + set(value) { + field = value + value?.run { resetLayoutManager(this) } + } + + val scrollHelper = ScrollHelper() + + /**是否激活焦点过渡监听*/ + var enableFocusTransition = false + set(value) { + field = value + if (value) { + //激活绘图顺序 + isFocusable = true + isFocusableInTouchMode = true + isChildrenDrawingOrderEnabled = true + } + } + + constructor(context: Context) : super(context) { + initAttribute(context) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initAttribute(context, attrs) + } + + private fun initAttribute(context: Context, attributeSet: AttributeSet? = null) { + val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslRecyclerView) + typedArray.getString(R.styleable.DslRecyclerView_r_layout_manager)?.let { + layout = it + } + typedArray.getBoolean( + R.styleable.DslRecyclerView_r_enable_focus_transition, + enableFocusTransition + ) + typedArray.recycle() + + scrollHelper.attach(this) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (layoutManager == null) { + //layout属性的支持 + layout?.run { layout = this } + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + } + + /** + * 锁定滚动到目标位置 + * [position] 目标位置, 负数表示倒数第几个 + * [duration] 锁定多少毫秒 + * [config] 自定义配置 + * */ + fun lockScroll( + position: Int = NO_POSITION, + duration: Long = -1, + config: ScrollHelper.LockDrawListener .() -> Unit = {} + ) { + scrollHelper.lockPositionByDraw { + lockPosition = position + lockDuration = duration + config() + } + } + + // + + override fun onChildDetachedFromWindow(child: View) { + super.onChildDetachedFromWindow(child) + if (child == _lastFocusViewRef?.get()) { + //清理焦点 + setFocusView(this, null) + clearChildFocus(child) + } + } + + /**这个方法可以改变child绘制的顺序. + * [childCount] 表示当前界面需要绘制的child数量 + * [drawingPosition] 表示当前需要绘制的child位置 + * @return 返回值表示, [drawingPosition]位置真正应该绘制的[drawingPosition]. + * * */ + override fun getChildDrawingOrder(childCount: Int, drawingPosition: Int): Int { + _lastFocusViewRef?.get()?.let { focusView -> + val focusIndex = indexOfChild(focusView) + if (focusIndex == -1) { + return drawingPosition + } + return when { + drawingPosition == childCount - 1 -> focusIndex + drawingPosition < focusIndex -> drawingPosition + else -> drawingPosition + 1 + } + } + return super.getChildDrawingOrder(childCount, drawingPosition) + } + + /**在层级结构中, 查找当前具有焦点的[View]*/ + override fun findFocus(): View? { + return super.findFocus()?.apply { + if (enableFocusTransition) { + setFocusView(this@DslRecyclerView, this) + } + } + } + + /**通过当前具有焦点[focused]的[View], 按照[direction]方向查找焦点*/ + override fun focusSearch(focused: View, direction: Int): View? { + return super.focusSearch(focused, direction)?.apply { + if (enableFocusTransition) { + ViewCompat.postInvalidateOnAnimation(this@DslRecyclerView) + } + } + } + + override fun focusSearch(direction: Int): View? { + return super.focusSearch(direction)?.apply { + //L.v(this) + } + } + + override fun dispatchUnhandledMove(focused: View?, direction: Int): Boolean { + //L.v("$focused $direction") + return super.dispatchUnhandledMove(focused, direction) + } + + override fun restoreDefaultFocus(): Boolean { + //L.v("...") + return super.restoreDefaultFocus() + } + + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + //L.v(event) + return try { + super.dispatchKeyEvent(event) + } catch (e: Exception) { + e.printStackTrace() + true + } + } + + override fun dispatchKeyEventPreIme(event: KeyEvent?): Boolean { + //L.v(event) + return super.dispatchKeyEventPreIme(event) + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + //L.d("keyCode:$keyCode") + return super.onKeyDown(keyCode, event) + } + + override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { + //L.d("keyCode:$keyCode") + return super.onKeyLongPress(keyCode, event) + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + //L.d("keyCode:$keyCode") + return super.onKeyUp(keyCode, event) + } + + override fun requestChildFocus(child: View?, focused: View?) { + super.requestChildFocus(child, focused) + //L.v("$child $focused") + } + + override fun requestChildRectangleOnScreen( + child: View, + rect: Rect, + immediate: Boolean + ): Boolean { + //L.v() + return super.requestChildRectangleOnScreen(child, rect, immediate) + } + + override fun addFocusables(views: ArrayList?, direction: Int, focusableMode: Int) { + //L.v(views) + super.addFocusables(views, direction, focusableMode) + +// val old = _lastFocusViewRef?.get() +// if (this.hasFocus() || old == null) { +// super.addFocusables(views, direction, focusableMode) +// } else { +// //将当前的view放到Focusable views列表中,再次移入焦点时会取到该view,实现焦点记忆功能 +// views?.add(old) +// } + } + + override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + //L.d("gainFocus:$gainFocus $direction $previouslyFocusedRect") + } + + override fun clearChildFocus(child: View?) { + super.clearChildFocus(child) + //L.d() + } + + override fun clearFocus() { + super.clearFocus() + //L.d() + } + + // + + // + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (ev.isTouchDown()) { + this.isTouchHold = true + } else if (ev.isTouchFinish()) { + this.isTouchHold = false + } + + getTouchActionDelegate().dispatchTouchEvent(ev) + + val result = super.dispatchTouchEvent(ev) + if (enableFocusTransition && ev.isTouchFinish()) { + //如果是通过TouchEvent改变的focus, 则需要手动触发一次[findFocus] + findFocus() + } + return result + } + + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + getTouchActionDelegate().onInterceptTouchEvent(ev) + return super.onInterceptTouchEvent(ev) + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + getTouchActionDelegate().onTouchEvent(ev) + return super.onTouchEvent(ev) + } + + override fun getTouchActionDelegate(): TouchActionDelegate { + return _touchDelegate + } + + /**是否还在touch中*/ + override var isTouchHold: Boolean = false + + // + + // + + override fun dispatchNestedScroll( + dxConsumed: Int, + dyConsumed: Int, + dxUnconsumed: Int, + dyUnconsumed: Int, + offsetInWindow: IntArray? + ): Boolean { + return super.dispatchNestedScroll( + dxConsumed, + dyConsumed, + dxUnconsumed, + dyUnconsumed, + offsetInWindow + ) + } + + // + +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/GridLayoutManagerWrap.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/GridLayoutManagerWrap.kt new file mode 100644 index 0000000..d1ffabd --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/GridLayoutManagerWrap.kt @@ -0,0 +1,43 @@ +package com.angcyo.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2020/01/02 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +class GridLayoutManagerWrap : GridLayoutManager { + + constructor(context: Context, spanCount: Int) : super(context, spanCount) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + constructor( + context: Context, + spanCount: Int, + orientation: Int, + reverseLayout: Boolean + ) : super(context, spanCount, orientation, reverseLayout) + + override fun onLayoutChildren( + recycler: RecyclerView.Recycler, + state: RecyclerView.State + ) { + try { + super.onLayoutChildren(recycler, state) + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/LinearLayoutManagerWrap.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/LinearLayoutManagerWrap.kt new file mode 100644 index 0000000..61bd35f --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/LinearLayoutManagerWrap.kt @@ -0,0 +1,45 @@ +package com.angcyo.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2020/01/02 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ + +class LinearLayoutManagerWrap : LinearLayoutManager { + + constructor(context: Context) : super(context) + + constructor( + context: Context, + orientation: Int = RecyclerView.VERTICAL + ) : super(context, orientation, false) + + constructor( + context: Context, + orientation: Int, + reverseLayout: Boolean + ) : super(context, orientation, reverseLayout) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { + try { + super.onLayoutChildren(recycler, state) + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/RecyclerBottomLayout.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/RecyclerBottomLayout.kt deleted file mode 100644 index 988b148..0000000 --- a/dslitem/src/main/java/com/angcyo/widget/recycler/RecyclerBottomLayout.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.angcyo.widget.recycler - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView - -/** - * 此布局会占满RecycleView第一屏的底部所有空间 - * - * - * Email:angcyo@126.com - * - * @author angcyo - * @date 2018/10/09 - */ -class RecyclerBottomLayout( - context: Context, attrs: AttributeSet? = null -) : FrameLayout(context, attrs) { - - var _layoutMeasureHeight = -1 - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val heightMode = MeasureSpec.getMode(heightMeasureSpec) - if (heightMode != MeasureSpec.EXACTLY) { - _layoutMeasureHeight = measuredHeight - } - } - - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - val parent = parent - //Log.w("angcyo", "layout:" + top + " " + bottom); - var callSuper = true - if (parent is RecyclerView) { - if (parent.scrollState != RecyclerView.SCROLL_STATE_IDLE) { - //滚动过程不处理 - super.onLayout(changed, left, top, right, bottom) - return - } - - val layoutParams = layoutParams as RecyclerView.LayoutParams - val parentHeight = parent.measuredHeight - //只处理第一屏 - if (parent.computeVerticalScrollOffset() == 0 && - top < parentHeight /*布局有部分展示了*/ && - bottom > top - ) { - if (bottom + layoutParams.bottomMargin != parentHeight) { //布局未全部展示 - //当前布局在RecyclerView的第一屏(没有任何滚动的状态), 并且底部没有显示全. - var spaceHeight = parentHeight - top - layoutParams.bottomMargin - var handle: Boolean - if (_layoutMeasureHeight > 0) { - handle = - spaceHeight - layoutParams.topMargin - layoutParams.bottomMargin > _layoutMeasureHeight - if (!handle) { //如果缓存了布局, 会出现此情况. 高度变高后, 无法回退到真实高度 - if (_layoutMeasureHeight != measuredHeight) { - spaceHeight = _layoutMeasureHeight - handle = true - } - } - } else { - handle = - spaceHeight - layoutParams.topMargin - layoutParams.bottomMargin > bottom - top - } - if (handle) { //剩余空间足够大, 同时也解决了动态隐藏导航栏带来的BUG - callSuper = false - layoutParams.height = spaceHeight - setLayoutParams(layoutParams) - post { - //Log.e("angcyo", "重置高度:" + layoutParams.height); - val adapter = parent.adapter - if (adapter != null) { - adapter.notifyItemChanged(layoutParams.viewAdapterPosition) - } else { - requestLayout() - } - } - } - } - } - } - if (callSuper) { - super.onLayout(changed, left, top, right, bottom) - } - } -} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/RecyclerEx.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/RecyclerEx.kt new file mode 100644 index 0000000..d7ea324 --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/RecyclerEx.kt @@ -0,0 +1,533 @@ +package com.angcyo.widget.recycler + +import android.text.TextUtils +import android.view.View +import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.RecyclerView.* +import com.angcyo.dsladapter.DslAdapter +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslItemDecoration +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.dsladapter.HoverItemDecoration +import com.angcyo.dsladapter.dslSpanSizeLookup +import com.angcyo.dsladapter.payload +import com.angcyo.item.getCurrVelocity +import com.angcyo.item.getMember + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/26 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ + +// + +/**清空原有的[ItemDecoration]*/ +fun RecyclerView.clearItemDecoration() { + for (i in itemDecorationCount - 1 downTo 0) { + removeItemDecorationAt(i) + } +} + +/**判断[RecyclerView]是否已经存在指定的[ItemDecoration]*/ +fun RecyclerView.haveItemDecoration(predicate: (ItemDecoration) -> Boolean): Boolean { + var result = false + for (i in 0 until itemDecorationCount) { + val itemDecoration = getItemDecorationAt(i) + if (predicate(itemDecoration)) { + result = true + break + } + } + return result +} + +/**移除指定的[ItemDecoration]*/ +fun RecyclerView.removeItemDecoration(predicate: (ItemDecoration) -> Boolean): Boolean { + var result = false + for (i in itemDecorationCount - 1 downTo 0) { + val itemDecoration = getItemDecorationAt(i) + if (predicate(itemDecoration)) { + removeItemDecorationAt(i) + result = true + } + } + return result +} + +/**[DslAdapter]必备的组件*/ +fun RecyclerView.initDsl() { + var haveItemDecoration = false + var haveHoverItemDecoration = false + for (i in 0 until itemDecorationCount) { + val itemDecoration = getItemDecorationAt(i) + if (itemDecoration is DslItemDecoration) { + haveItemDecoration = true + } else if (itemDecoration is HoverItemDecoration) { + haveHoverItemDecoration = true + } + } + if (!haveItemDecoration) { + DslItemDecoration().attachToRecyclerView(this) + } + if (!haveHoverItemDecoration) { + HoverItemDecoration().attachToRecyclerView(this) + } +} + +/** + * [initDslAdapter] + * dsl在[com.angcyo.dsladapter.DslAdapter.render]方法中执行 + * */ +fun RecyclerView.renderDslAdapter( + append: Boolean = false, //当已经是adapter时, 是否追加item. 需要先关闭 new + new: Boolean = true, //始终创建新的adapter, 为true时, 则append无效 + updateState: Boolean = true, + action: DslAdapter.() -> Unit = {} +) { + initDslAdapter(append, new) { + render(updateState) { + action() + } + } +} + +/**快速初始化[DslAdapter] + * [initDsl] + * [dslAdapter]*/ +fun RecyclerView.initDslAdapter( + append: Boolean = false, //当已经是adapter时, 是否追加item. 需要先关闭 new + new: Boolean = true, //始终创建新的adapter, 为true时, 则append无效 + action: DslAdapter.() -> Unit = {} +): DslAdapter { + initDsl() + if (layoutManager == null) { + resetLayoutManager("v") + } + return dslAdapter(append, new, action) +} + +fun RecyclerView.dslAdapter( + append: Boolean = false, //当已经是adapter时, 是否追加item. 需要先关闭 new + new: Boolean = true, //始终创建新的adapter, 为true时, 则append无效 + init: DslAdapter.() -> Unit +): DslAdapter { + + var dslAdapter: DslAdapter? = null + + fun newAdapter() { + dslAdapter = DslAdapter() + adapter = dslAdapter + + dslAdapter!!.init() + } + + if (new) { + newAdapter() + } else { + if (adapter is DslAdapter) { + dslAdapter = adapter as DslAdapter + + if (!append) { + dslAdapter!!.clearItems() + } + + dslAdapter!!.init() + } else { + newAdapter() + } + } + + return dslAdapter!! +} + +// + +// + +/** 通过[V] [H] [GV2] [GH3] [SV2] [SV3] 方式, 设置 [LayoutManager] */ +fun RecyclerView.resetLayoutManager(match: String) { + val oldLayoutManager = layoutManager + var layoutManager: LayoutManager? = null + var spanCount = 1 + var orientation = VERTICAL + + if (TextUtils.isEmpty(match) || "V".equals(match, ignoreCase = true)) { + if (oldLayoutManager is LinearLayoutManagerWrap) { + if (oldLayoutManager.orientation != LinearLayoutManager.VERTICAL) { + layoutManager = + LinearLayoutManagerWrap(context, LinearLayoutManager.VERTICAL, false) + } + } else { + layoutManager = LinearLayoutManagerWrap(context, LinearLayoutManager.VERTICAL, false) + } + } else { + //线性布局管理器 + if ("H".equals(match, ignoreCase = true)) { + if (oldLayoutManager is LinearLayoutManagerWrap) { + if (oldLayoutManager.orientation != LinearLayoutManager.HORIZONTAL) { + layoutManager = + LinearLayoutManagerWrap(context, LinearLayoutManager.HORIZONTAL, false) + } + } else { + layoutManager = + LinearLayoutManagerWrap(context, LinearLayoutManager.HORIZONTAL, false) + } + } else { //读取其他配置信息(数量和方向) + val type = match.substring(0, 1) + if (match.length >= 3) { + try { + spanCount = Integer.valueOf(match.substring(2)) //数量 + } catch (e: Exception) { + } + } + if (match.length >= 2) { + if ("H".equals(match.substring(1, 2), ignoreCase = true)) { + orientation = StaggeredGridLayoutManager.HORIZONTAL //方向 + } + } + //交错布局管理器 + if ("S".equals(type, ignoreCase = true)) { + if (oldLayoutManager is StaggeredGridLayoutManagerWrap) { + if (oldLayoutManager.spanCount != spanCount || oldLayoutManager.orientation != orientation) { + layoutManager = + StaggeredGridLayoutManagerWrap( + spanCount, + orientation + ) + } + } else { + layoutManager = + StaggeredGridLayoutManagerWrap( + spanCount, + orientation + ) + } + } else if ("G".equals(type, ignoreCase = true)) { + if (oldLayoutManager is GridLayoutManagerWrap) { + if (oldLayoutManager.spanCount != spanCount || oldLayoutManager.orientation != orientation) { + layoutManager = + GridLayoutManagerWrap( + context, + spanCount, + orientation, + false + ) + } + } else { + layoutManager = + GridLayoutManagerWrap( + context, + spanCount, + orientation, + false + ) + } + } + } + } + + if (layoutManager is GridLayoutManager) { + val gridLayoutManager = layoutManager + gridLayoutManager.dslSpanSizeLookup(this) + } else if (layoutManager is LinearLayoutManager) { + layoutManager.recycleChildrenOnDetach = true + } + + if (layoutManager != null) { + this.layoutManager = layoutManager + } +} + +/** + * 取消RecyclerView的默认动画 + * */ +fun RecyclerView.noItemAnim(animator: ItemAnimator? = null) { + itemAnimator = animator +} + +/** + * 取消默认的change动画 + * */ +fun RecyclerView.noItemChangeAnim(no: Boolean = true) { + if (itemAnimator == null) { + itemAnimator = DefaultItemAnimator().apply { + supportsChangeAnimations = !no + } + } else if (itemAnimator is SimpleItemAnimator) { + (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = + !no + } +} + +/** + * 设置RecyclerView的默认动画 + * */ +fun RecyclerView.setItemAnim(animator: ItemAnimator? = DefaultItemAnimator()) { + itemAnimator = animator +} + +/**第一个item是否完全可见*/ +fun RecyclerView.isFirstItemVisibleCompleted(): Boolean { + val linearLayoutManager = layoutManager as? LinearLayoutManager? ?: return false + val firstPosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition() + return firstPosition == 0 +} + +/**最后一个item是否完全可见*/ +fun RecyclerView.isLastItemVisibleCompleted(): Boolean { + val linearLayoutManager = layoutManager as? LinearLayoutManager? ?: return false + val lastPosition = linearLayoutManager.findLastCompletelyVisibleItemPosition() + val childCount = linearLayoutManager.childCount + val firstPosition = linearLayoutManager.findFirstVisibleItemPosition() + return lastPosition >= firstPosition + childCount - 1 +} + +/**列表中所有的item, 均已显示*/ +fun RecyclerView.isAllItemVisibleCompleted(): Boolean { + val linearLayoutManager = layoutManager as? LinearLayoutManager? ?: return false + val firstPosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition() + val lastPosition = linearLayoutManager.findLastCompletelyVisibleItemPosition() + + if (firstPosition == 0 && lastPosition - firstPosition <= adapter?.itemCount ?: 0) { + return true + } + return false +} + +/**在列表可滚动的情况下, 顶部item已经全部显示*/ +fun RecyclerView.isTopItemVisibleCompleted(): Boolean { + val linearLayoutManager = layoutManager as? LinearLayoutManager? ?: return false + val firstPosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition() + val lastPosition = linearLayoutManager.findLastCompletelyVisibleItemPosition() + + if (firstPosition == 0 && lastPosition + 1 < adapter?.itemCount ?: 0) { + return true + } + return false +} + +// + +// + +/** + * 获取[RecyclerView]指定位置[index]的[DslViewHolder], 负数表示倒数开始的index + * [isLayoutIndex] 界面上存在, 类似 [LayoutPosition] [AdapterPosition] 的区别 + * */ +operator fun RecyclerView.get(index: Int, isLayoutIndex: Boolean = false): DslViewHolder? { + + var result: DslViewHolder? + + if (isLayoutIndex) { + val layoutIndex = if (index >= 0) { + //正向取child + index + } else { + //反向取child + childCount + index + } + + result = findViewHolderForLayoutPosition(layoutIndex) as? DslViewHolder + if (result == null) { + val child: View? = getChildAt(layoutIndex) + result = child?.run { getChildViewHolder(this) as? DslViewHolder } + } + } else { + val adapterIndex = if (index >= 0) { + //正向取child + index + } else { + //反向取child + (adapter?.itemCount ?: 0) + index + } + + result = findViewHolderForAdapterPosition(adapterIndex) as? DslViewHolder + } + + return result +} + +/**获取[RecyclerView]界面上存在的所有[DslViewHolder]*/ +fun RecyclerView.allViewHolder(): List { + val result = mutableListOf() + for (i in 0 until childCount) { + val child = getChildAt(i) + (getChildViewHolder(child) as? DslViewHolder)?.run { result.add(this) } + } + return result +} + +/**本地更新[RecyclerView]界面, + * [position] 指定需要更新的位置, 负数表示全部*/ +fun RecyclerView.localUpdateItem(position: Int = -1, payloads: List = payload()) { + if (adapter !is DslAdapter) { + return + } + val allViewHolder = allViewHolder() + if (position >= allViewHolder.size) { + return + } + allViewHolder.forEach { viewHolder -> + val adapterPosition = viewHolder.adapterPosition + val adapterItem = (adapter as DslAdapter).getItemData(adapterPosition) + adapterItem?.run { + if (position >= 0 && position == adapterPosition) { + //只更新指定的位置 + itemBind(viewHolder, adapterPosition, adapterItem, payloads) + return + } else { + itemBind(viewHolder, adapterPosition, adapterItem, payloads) + } + } + } +} + +fun RecyclerView.localUpdateItem(action: (adapterItem: DslAdapterItem, itemHolder: DslViewHolder, itemPosition: Int) -> Unit) { + if (adapter !is DslAdapter) { + return + } + allViewHolder().forEach { viewHolder -> + val adapterPosition = viewHolder.adapterPosition + val adapterItem = (adapter as DslAdapter).getItemData(adapterPosition) + adapterItem?.run { + if (adapterPosition >= 0) { + action(adapterItem, viewHolder, adapterPosition) + } + } + } +} +// + +// + +/** + * 获取[RecyclerView] [Fling] 时的速率 + * */ +fun RecyclerView?.getLastVelocity(): Float { + var currVelocity = 0f + try { + val mViewFlinger = this.getMember(RecyclerView::class.java, "mViewFlinger") + var mScroller = mViewFlinger.getMember("mScroller") + if (mScroller == null) { + mScroller = mViewFlinger.getMember("mOverScroller") + } + currVelocity = mScroller.getCurrVelocity() + } catch (e: Exception) { + e.printStackTrace() + } + return currVelocity +} + +fun Int.scrollStateStr(): String { + return when (this) { + SCROLL_STATE_SETTLING -> "SCROLL_STATE_SETTLING" + SCROLL_STATE_DRAGGING -> "SCROLL_STATE_DRAGGING" + SCROLL_STATE_IDLE -> "SCROLL_STATE_IDLE" + else -> "Unknown" + } +} + +fun RecyclerView.scrollHelper(action: ScrollHelper.() -> Unit = {}): ScrollHelper { + return ScrollHelper().apply { + attach(this@scrollHelper) + action() + } +} + +/**滚动到尾部*/ +fun RecyclerView.scrollToEnd(smooth: Boolean = false) { + val count = adapter?.itemCount ?: 0 + val position = count - 1 + if (position in 0 until count) { + if (smooth) { + smoothScrollToPosition(position) + } else { + scrollToPosition(position) + } + } +} + +/**滚动到顶部*/ +fun RecyclerView.scrollToFirst(smooth: Boolean = false) { + val count = adapter?.itemCount ?: 0 + if (count > 0) { + val position = 0 + if (smooth) { + smoothScrollToPosition(position) + } else { + scrollToPosition(position) + } + } +} + +/**保存当前的滚动位置*/ +fun RecyclerView.saveScrollPosition(): ScrollPositionConfig { + val result = ScrollPositionConfig() + + if (childCount > 0) { + val childAt = getChildAt(0) + val layoutParams = childAt.layoutParams as LayoutParams + + result.adapterPosition = layoutParams.viewAdapterPosition + + result.left = layoutManager?.getDecoratedLeft(childAt) ?: 0 + result.top = layoutManager?.getDecoratedTop(childAt) ?: 0 + } + + return result +} + +/**恢复滚动位置*/ +fun RecyclerView.restoreScrollPosition(config: ScrollPositionConfig) { + if (config.adapterPosition >= 0) { + val lm = layoutManager + when (lm) { + is LinearLayoutManager -> lm.scrollToPositionWithOffset( + config.adapterPosition, + if (lm.orientation == HORIZONTAL) config.left else config.top + ) + + is StaggeredGridLayoutManager -> lm.scrollToPositionWithOffset( + config.adapterPosition, + if (lm.orientation == HORIZONTAL) config.left else config.top + ) + + else -> scrollToPosition(config.adapterPosition) + } + } +} + +/**监听滚动状态改变*/ +fun RecyclerView.onScrollStateChangedAction(action: (recyclerView: RecyclerView, newState: Int) -> Unit): OnScrollListener { + val listener = object : OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + action(recyclerView, newState) + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + } + } + addOnScrollListener(listener) + return listener +} + +/**监听滚动改变*/ +fun RecyclerView.onScrolledAction(action: (recyclerView: RecyclerView, dx: Int, dy: Int) -> Unit): OnScrollListener { + val listener = object : OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + action(recyclerView, dx, dy) + } + } + addOnScrollListener(listener) + return listener +} + +// \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/ScrollHelper.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/ScrollHelper.kt new file mode 100644 index 0000000..ab7aece --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/ScrollHelper.kt @@ -0,0 +1,833 @@ +package com.angcyo.widget.recycler + +import android.os.Build +import android.view.View +import android.view.ViewTreeObserver +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.angcyo.dpi +import com.angcyo.dsladapter.L +import com.angcyo.dsladapter.mH +import com.angcyo.dsladapter.nowTime +import com.angcyo.widget.recycler.ScrollHelper.Companion.DEFAULT_SCROLL_STEP +import kotlin.math.absoluteValue +import kotlin.math.min + +/** + * 滚动机制: + * 1. 不带动画滚动至位置可见 scrollToPosition + * 2. 带动画滚动至位置可见 smoothScrollToPosition + * + * 上述2个方法, 均无法准确控制position, 只要position出现在界面即完成滚动. + * + * 1. 不带动画精确滚动 scrollBy + * 2. 带动画精确滚动 smoothScrollBy + * + * 上述2个方法, 可以精确控制position出现在界面上的位置. 比如 置顶, 尾部, 居中等. + * 但是, 当position没有出现在界面上时, 很难使用这2个方法精确控制. + * + * 综上: 要想精确控制position, 应先使用[scrollToPosition or smoothScrollToPosition]保证目标 + * position可见, 再使用[scrollBy or smoothScrollBy]精确控制目标的位置 + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/09/28 + */ +class ScrollHelper { + + companion object { + /**滚动类别: 默认不特殊处理. 滚动到item显示了就完事*/ + const val SCROLL_TYPE_NORMAL = 0 + + /**滚动类别: 将item滚动到第一个位置*/ + const val SCROLL_TYPE_TOP = 1 + + /**滚动类别: 将item滚动到最后一个位置*/ + const val SCROLL_TYPE_BOTTOM = 2 + + /**滚动类别: 将item滚动到居中位置*/ + const val SCROLL_TYPE_CENTER = 3 + + var DEFAULT_SCROLL_STEP = 16 + + var DEFAULT_ANIM_DELAY = 16L + } + + internal var recyclerView: RecyclerView? = null + + fun attach(recyclerView: RecyclerView) { + if (this.recyclerView == recyclerView) { + return + } + detach() + this.recyclerView = recyclerView + } + + fun detach() { + recyclerView = null + } + + fun itemCount(): Int { + return recyclerView?.layoutManager?.itemCount ?: 0 + } + + fun lastItemPosition(): Int { + return itemCount() - 1 + } + + /**负数表示反序, 倒数第几个*/ + fun parsePosition(position: Int): Int { + return if (position < 0) { + itemCount() + position + } else { + position + } + } + + fun scrollToLast( + scrollParams: ScrollParams = _defaultScrollParams().apply { + scrollType = SCROLL_TYPE_BOTTOM + isFromAddItem + }, action: ScrollParams.() -> Unit = {} + ) { + scrollParams.action() + startScroll(lastItemPosition(), scrollParams) + } + + fun _defaultScrollParams(): ScrollParams { + return ScrollParams() + } + + fun startScroll(scrollParams: ScrollParams = _defaultScrollParams()) { + startScroll(scrollParams.scrollPosition, scrollParams) + } + + fun scroll(position: Int, scrollParams: ScrollParams = _defaultScrollParams()) { + startScroll(position, scrollParams) + } + + fun startScroll(position: Int, scrollParams: ScrollParams = _defaultScrollParams()) { + val targetPosition = parsePosition(position) + if (check(targetPosition)) { + scrollParams.scrollPosition = targetPosition + + if (scrollParams.stepScroll) { + //步进滚动 + scrollWithStep(scrollParams) + } else { + recyclerView?.stopScroll() + + if (isPositionVisible(targetPosition)) { + //目标可见时的滚动 + scrollWithVisible(scrollParams) + } else { + //先滚动出目标 + scrollWithNoVisible(scrollParams) + } + } + } + } + + private var lockLayoutListener: LockLayoutListener? = null + + /**短时间之内, 锁定滚动到0的位置*/ + fun lockScrollToFirst(config: LockDrawListener.() -> Unit = {}) { + lockPositionByDraw { + scrollType = SCROLL_TYPE_TOP + lockPosition = 0 + firstScrollAnim = true + scrollAnim = true + force = true + firstForce = true + enableLock = true + lockDuration = 60 + autoDetach = true + isFromAddItem = false + config() + } + } + + /**短时间之内, 锁定滚动到倒数第一个的位置*/ + fun lockScrollToLast(config: LockDrawListener.() -> Unit = {}) { + lockPositionByDraw { + scrollType = SCROLL_TYPE_BOTTOM + lockPosition = -1 + firstScrollAnim = true + scrollAnim = true + force = true + firstForce = true + enableLock = true + lockDuration = 60 + autoDetach = true + isFromAddItem = false + config() + } + } + + /** + * 当界面有变化时, 自动滚动到最后一个位置 + * [unlockPosition] + * */ + fun lockPosition(config: LockLayoutListener.() -> Unit = {}): LockLayoutListener? { + var result: LockLayoutListener? = null + if (lockLayoutListener == null && recyclerView != null) { + lockLayoutListener = LockLayoutListener().apply { + scrollType = SCROLL_TYPE_CENTER + autoDetach = true + config() + attach(recyclerView!!) + } + result = lockLayoutListener + } + return result + } + + fun lockPositionByDraw(config: LockDrawListener.() -> Unit = {}): LockDrawListener? { + var result: LockDrawListener? = null + recyclerView?.let { + result = LockDrawListener().apply { + //默认将目标滚动到中间位置 + scrollType = SCROLL_TYPE_CENTER + autoDetach = true + config() + attach(it) + } + it.postInvalidateOnAnimation() + } + return result + } + + fun lockPositionByLayout(config: LockLayoutListener.() -> Unit = {}): LockLayoutListener? { + var result: LockLayoutListener? = null + recyclerView?.let { + result = LockLayoutListener().apply { + scrollType = SCROLL_TYPE_CENTER + autoDetach = true + config() + attach(it) + } + it.requestLayout() + } + return result + } + + fun unlockPosition() { + lockLayoutListener?.detach() + lockLayoutListener = null + } + + internal fun scrollWithNoVisible(scrollParams: ScrollParams) { + val targetPosition = parsePosition(scrollParams.scrollPosition) + if (scrollParams.scrollAnim) { + if (scrollParams.isFromAddItem) { + if (recyclerView?.itemAnimator is SimpleItemAnimator) { + //itemAnimator 自带动画 + recyclerView?.scrollToPosition(targetPosition) + } else { + recyclerView?.smoothScrollToPosition(targetPosition) + } + } else { + recyclerView?.smoothScrollToPosition(targetPosition) + } + } else { + if (scrollParams.isFromAddItem) { + val itemAnimator = recyclerView?.itemAnimator + if (itemAnimator != null) { + //有默认的动画 + recyclerView?.itemAnimator = null + OnNoAnimScrollIdleListener(itemAnimator).attach(recyclerView!!) + } + } + recyclerView?.scrollToPosition(targetPosition) + } + if (scrollParams.scrollType != SCROLL_TYPE_NORMAL) { + //不可见时, 需要现滚动到可见位置, 再进行微调 + OnScrollIdleListener(scrollParams).attach(recyclerView!!) + } + } + + /**当需要滚动的目标位置已经在屏幕上可见*/ + internal fun scrollWithVisible(scrollParams: ScrollParams) { + when (scrollParams.scrollType) { + SCROLL_TYPE_NORMAL -> { + //nothing + } + + SCROLL_TYPE_TOP -> { + viewByPosition(scrollParams.scrollPosition)?.also { child -> + recyclerView?.apply { + val dx = layoutManager!!.getDecoratedLeft(child) - + paddingLeft - scrollParams.scrollOffset + + val dy = layoutManager!!.getDecoratedTop(child) - + paddingTop - scrollParams.scrollOffset + + if (scrollParams.scrollAnim) { + smoothScrollBy(dx, dy) + } else { + scrollBy(dx, dy) + } + } + } + } + + SCROLL_TYPE_BOTTOM -> { + viewByPosition(scrollParams.scrollPosition)?.also { child -> + recyclerView?.apply { + val dx = + layoutManager!!.getDecoratedRight(child) - + measuredWidth + paddingRight + scrollParams.scrollOffset + val dy = + layoutManager!!.getDecoratedBottom(child) - + measuredHeight + paddingBottom + scrollParams.scrollOffset + + if (scrollParams.scrollAnim) { + smoothScrollBy(dx, dy) + } else { + scrollBy(dx, dy) + } + } + } + } + + SCROLL_TYPE_CENTER -> { + viewByPosition(scrollParams.scrollPosition)?.also { child -> + + recyclerView?.apply { + val recyclerCenterX = + (measuredWidth - paddingLeft - paddingRight) / 2 + paddingLeft + + val recyclerCenterY = + (measuredHeight - paddingTop - paddingBottom) / 2 + paddingTop + + val dx = layoutManager!!.getDecoratedLeft(child) - recyclerCenterX + + layoutManager!!.getDecoratedMeasuredWidth(child) / 2 + scrollParams.scrollOffset + + val dy = layoutManager!!.getDecoratedTop(child) - recyclerCenterY + + layoutManager!!.getDecoratedMeasuredHeight(child) / 2 + scrollParams.scrollOffset + + if (scrollParams.scrollAnim) { + smoothScrollBy(dx, dy) + } else { + scrollBy(dx, dy) + } + } + } + } + } + } + + internal fun scrollWithStep(scrollParams: ScrollParams) { + var dx = 0 + var dy = scrollParams.stepScrollSize * dpi + + val scrollPosition = scrollParams.scrollPosition + + //结束时, view的top坐标 + var endViewTop = 0 + + //当前view的top坐标 + var currentViewTop = 0 + + var refPosition = RecyclerView.NO_POSITION + + when (scrollParams.scrollType) { + SCROLL_TYPE_TOP -> { + endViewTop = recyclerView?.paddingTop ?: 0 + refPosition = recyclerView.findFirstVisibleItemPosition() + } + + SCROLL_TYPE_BOTTOM -> { + endViewTop = recyclerView.mH() - (recyclerView?.paddingBottom ?: 0) - + viewByPosition(scrollPosition).mH() + + refPosition = recyclerView.findLastVisibleItemPosition() + } + + SCROLL_TYPE_CENTER -> { + endViewTop = (recyclerView?.paddingTop ?: 0) + recyclerView.mH() / 2 - + viewByPosition(scrollPosition).mH() / 2 + refPosition = recyclerView.findFirstVisibleItemPosition() + } + + else -> { + if (isPositionVisible(scrollPosition)) { + //目标可见, 停止滚动 + dx = 0 + dy = 0 + } + } + } + + if (refPosition != RecyclerView.NO_POSITION) { + if ((refPosition - scrollPosition).absoluteValue > 1) { + //相差1个以上, 关闭step + scrollWithNoVisible(scrollParams) + return + } + + currentViewTop = when { + isPositionVisible(scrollPosition) -> recyclerView.getPositionTop(scrollPosition) + scrollPosition > refPosition -> Int.MAX_VALUE + else -> Int.MIN_VALUE + } + + if (currentViewTop > endViewTop) { + dy = min(dy, currentViewTop - endViewTop) + } else { + dy = -min(dy, endViewTop - currentViewTop) + } + } + + if (scrollParams.scrollAnim) { + recyclerView?.smoothScrollBy(dx, dy) + } else { + recyclerView?.scrollBy(dx, dy) + } + } + + /**是否滚动到了目标*/ + private fun isScrollToPosition(targetPosition: Int, scrollType: Int): Boolean { + val scrollPosition = parsePosition(targetPosition) + return when (scrollType) { + SCROLL_TYPE_TOP -> recyclerView.getPositionTop(scrollPosition) == 0 + SCROLL_TYPE_BOTTOM -> recyclerView.getPositionBottom(scrollPosition) == recyclerView.mH() + SCROLL_TYPE_CENTER -> recyclerView.getPositionTop(scrollPosition) == + (recyclerView.mH() - viewByPosition(scrollPosition).mH()) / 2 + + else -> isPositionVisible(scrollPosition) + } + } + + /**位置是否可见*/ + private fun isPositionVisible(position: Int): Boolean { + return recyclerView?.layoutManager.isPositionVisible(position) + } + + private fun viewByPosition(position: Int): View? { + return recyclerView?.layoutManager?.findViewByPosition(position) + } + + /**检查是否可以操作滚动*/ + private fun check(position: Int): Boolean { + if (recyclerView == null) { + L.e("请先调用[attach]方法.") + return false + } + + if (recyclerView?.adapter == null) { + L.w("忽略, [adapter] is null") + return false + } + + if (recyclerView?.layoutManager == null) { + L.w("忽略, [layoutManager] is null") + return false + } + + val itemCount = itemCount() + val p = parsePosition(position) + if (p < 0 || p >= itemCount) { + L.w("忽略, [position] 需要在 [0,$itemCount) 之间.") + return false + } + + return true + } + + fun log(recyclerView: RecyclerView? = this.recyclerView) { + recyclerView?.viewTreeObserver?.apply { + this.addOnDrawListener { + L.i("onDraw") + } + this.addOnGlobalFocusChangeListener { oldFocus, newFocus -> + L.i("on...$oldFocus ->$newFocus") + } + this.addOnGlobalLayoutListener { + L.w("this....") + } + //此方法回调很频繁 + this.addOnPreDrawListener { + //L.v("this....") + true + } + this.addOnScrollChangedListener { + L.i("this....${recyclerView.scrollState}") + } + this.addOnTouchModeChangeListener { + L.i("this....") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.addOnWindowFocusChangeListener { + L.i("this....") + } + } + } + } + + private abstract inner class OnScrollListener : ViewTreeObserver.OnScrollChangedListener, + IAttachListener { + var attachView: View? = null + + override fun attach(view: View) { + detach() + attachView = view + view.viewTreeObserver.addOnScrollChangedListener(this) + } + + override fun detach() { + attachView?.viewTreeObserver?.removeOnScrollChangedListener(this) + } + + override fun onScrollChanged() { + onScrollChanged(recyclerView?.scrollState ?: RecyclerView.SCROLL_STATE_IDLE) + detach() + } + + abstract fun onScrollChanged(state: Int) + } + + /**滚动结束之后, 根据类别, 继续滚动.*/ + private inner class OnScrollIdleListener(val scrollParams: ScrollParams) : + OnScrollListener() { + + override fun onScrollChanged(state: Int) { + if (state == RecyclerView.SCROLL_STATE_IDLE) { + scrollWithVisible(scrollParams) + } + } + } + + /**临时去掉动画滚动, 之后恢复动画*/ + private inner class OnNoAnimScrollIdleListener(val itemAnimator: RecyclerView.ItemAnimator?) : + OnScrollListener() { + + override fun onScrollChanged(state: Int) { + if (state == RecyclerView.SCROLL_STATE_IDLE) { + recyclerView?.itemAnimator = itemAnimator + } + } + } + + abstract inner class LockScrollListener : ViewTreeObserver.OnGlobalLayoutListener, + ViewTreeObserver.OnDrawListener, + IAttachListener, Runnable { + + /**激活滚动动画*/ + var scrollAnim: Boolean = true + set(value) { + field = value + if (!value && firstScrollAnim) { + firstScrollAnim = false + } + } + + /**激活第一个滚动的动画*/ + var firstScrollAnim: Boolean = true + + /**设置了[stepScroll]之后, [lockDuration]超时了, 但也还是会滚动到目标为止 + * 只在滚动目标的位置相差不太远时生效*/ + var stepScroll: Boolean = false + set(value) { + field = value + if (value) { + //激活step之后, 建议不要开始smooth滚动方式 + scrollAnim = false + } + } + + var stepScrollSize: Int = DEFAULT_SCROLL_STEP + + /**激活了滚动动画时, 调用延迟设置, + * 如果延迟设置的低, smooth相当于没动画了*/ + var animDelay: Long = DEFAULT_ANIM_DELAY + + /**不检查界面 情况, 强制滚动到最后的位置. 关闭后. 会智能判断*/ + var force: Boolean = false + + /**第一次时, 是否强制滚动. 先触发一次滚动, 之后再微调至目标*/ + var firstForce: Boolean = true + + /**滚动阈值, 倒数第几个可见时, 就允许滚动*/ + var scrollThreshold = 2 + + /**锁定需要滚动的position, 负数表示倒数第几个*/ + var lockPosition = RecyclerView.NO_POSITION + + var scrollType = SCROLL_TYPE_NORMAL + var scrollOffset = 0 + var isFromAddItem = true + + /**是否激活锁定滚动功能*/ + var enableLock = true + + /**滚动到目标后, 自动调用[detach]*/ + var autoDetach = false + + /**锁定时长, 毫秒 + * 这段时间之内, 都会触发滚动*/ + var lockDuration: Long = -1 + + //记录开始的统计时间 + var _lockStartTime = 0L + + override fun run() { + + val itemCount = itemCount() + if (!enableLock || itemCount <= 0) { + return + } + + val isScrollAnim = if (firstForce) firstScrollAnim && scrollAnim else scrollAnim + + val position = parsePosition(lockPosition) + + val scrollParams = ScrollParams( + position, + scrollType, + isScrollAnim, + scrollOffset, + isFromAddItem, + stepScroll, + stepScrollSize + ) + + if (force || firstForce || stepScroll) { + scroll(position, scrollParams) + onScrollTrigger() + L.i("锁定滚动至->$position $force $firstForce $stepScroll") + } else { + val lastItemPosition = lastItemPosition() + if (lastItemPosition != RecyclerView.NO_POSITION) { + //智能判断是否可以锁定 + if (position == 0) { + //滚动到顶部 + val findFirstVisibleItemPosition = + recyclerView?.layoutManager.findFirstVisibleItemPosition() + + if (findFirstVisibleItemPosition <= scrollThreshold) { + scroll(position, scrollParams) + onScrollTrigger() + L.i("锁定滚动至->$position") + } + } else { + val findLastVisibleItemPosition = + recyclerView?.layoutManager.findLastVisibleItemPosition() + + if (lastItemPosition - findLastVisibleItemPosition <= scrollThreshold) { + //最后第一个或者最后第2个可见, 智能判断为可以滚动到尾部 + scroll(position, scrollParams) + onScrollTrigger() + L.i("锁定滚动至->$position") + } + } + } + } + + firstForce = false + } + + var attachView: View? = null + + override fun attach(view: View) { + detach() + attachView = view + } + + override fun detach() { + attachView?.removeCallbacks(this) + } + + /**[ViewTreeObserver.OnDrawListener]*/ + override fun onDraw() { + initLockStartTime() + onLockScroll() + } + + /**[ViewTreeObserver.OnGlobalLayoutListener]*/ + override fun onGlobalLayout() { + initLockStartTime() + onLockScroll() + } + + open fun initLockStartTime() { + if (_lockStartTime <= 0) { + _lockStartTime = nowTime() + } + } + + open fun isLockTimeout(): Boolean { + if (stepScroll) { + //只有在目标可见的时候, 才统计时间 + if (isPositionVisible(parsePosition(lockPosition))) { + //目标可见 + } else { + _lockStartTime = nowTime() + return false + } + } + val timeout = if (lockDuration > 0) { + val nowTime = nowTime() + nowTime - _lockStartTime > lockDuration + } else { + false + } + + if (stepScroll) { + if (timeout) { + val nowTime = nowTime() + return isScrollToPosition( + lockPosition, + scrollType + ) || nowTime - _lockStartTime > lockDuration * 10 + } + } + return timeout + } + + /**是否第一个调用[post]*/ + var _isFirstPost = true + + open fun onLockScroll() { + //attachView?.removeCallbacks(this) + if (enableLock) { + if (isLockTimeout()) { + //锁定超时, 放弃操作 + if (autoDetach) { + attachView?.post { + detach() + } + } else { + L.w("锁定已超时, 跳过操作.") + } + } else { + if ((stepScroll || scrollAnim || firstScrollAnim) && animDelay > 0 && !_isFirstPost) { + attachView?.postDelayed(this, animDelay) + } else { + attachView?.post(this) + } + _isFirstPost = false + } + } + } + + open fun onScrollTrigger() { + if (autoDetach) { + if (isLockTimeout() || lockDuration == -1L) { + detach() + } + } + } + } + + /**锁定滚动到最后一个位置*/ + inner class LockLayoutListener : LockScrollListener() { + + override fun attach(view: View) { + super.attach(view) + view.viewTreeObserver.addOnGlobalLayoutListener(this) + } + + override fun detach() { + super.detach() + attachView?.viewTreeObserver?.removeOnGlobalLayoutListener(this) + } + } + + /**滚动到0*/ + inner class LockDrawListener : LockScrollListener() { + + override fun attach(view: View) { + super.attach(view) + view.viewTreeObserver.addOnDrawListener(this) + } + + override fun detach() { + super.detach() + attachView?.viewTreeObserver?.removeOnDrawListener(this) + } + } + + private interface IAttachListener { + fun attach(view: View) + + fun detach() + } +} + +//滚动参数 +data class ScrollParams( + /**滚动目标, 负数反向取值*/ + var scrollPosition: Int = RecyclerView.NO_POSITION, + /**滚动类型, [可见就行] [贴顶显示] [贴底显示] [居中显示]*/ + var scrollType: Int = ScrollHelper.SCROLL_TYPE_NORMAL, + /**是否需要动画*/ + var scrollAnim: Boolean = true, + /**滚动到当前位置时, 额外的偏移*/ + var scrollOffset: Int = 0, + /**是否由AddItem导致的偏移*/ + var isFromAddItem: Boolean = true, + /**步进滚动, 即使用[scrollBy] [smoothScrollBy]进行滚动*/ + var stepScroll: Boolean = false, + /**一次滚动多少距离, 会自动乘以 dpi*/ + var stepScrollSize: Int = DEFAULT_SCROLL_STEP, +) + +fun RecyclerView?.findFirstVisibleItemPosition(): Int { + return this?.layoutManager.findFirstVisibleItemPosition() +} + +/**获取目标位置child, 顶部的距离*/ +fun RecyclerView?.getPositionTop(position: Int, def: Int = Int.MAX_VALUE): Int { + val view = this?.layoutManager?.findViewByPosition(position) ?: return def + return layoutManager?.getDecoratedTop(view) ?: def +} + +fun RecyclerView?.getPositionBottom(position: Int, def: Int = Int.MAX_VALUE): Int { + val view = this?.layoutManager?.findViewByPosition(position) ?: return def + return layoutManager?.getDecoratedBottom(view) ?: def +} + +fun RecyclerView.LayoutManager?.findFirstVisibleItemPosition(): Int { + var result = RecyclerView.NO_POSITION + this?.also { layoutManager -> + var firstItemPosition: Int = -1 + if (layoutManager is LinearLayoutManager) { + firstItemPosition = layoutManager.findFirstVisibleItemPosition() + } else if (layoutManager is StaggeredGridLayoutManager) { + firstItemPosition = + layoutManager.findFirstVisibleItemPositions(null).firstOrNull() ?: -1 + } + result = firstItemPosition + } + return result +} + +fun RecyclerView?.findLastVisibleItemPosition(): Int { + return this?.layoutManager.findLastVisibleItemPosition() +} + +fun RecyclerView.LayoutManager?.findLastVisibleItemPosition(): Int { + var result = RecyclerView.NO_POSITION + this?.also { layoutManager -> + var lastItemPosition: Int = -1 + if (layoutManager is LinearLayoutManager) { + lastItemPosition = layoutManager.findLastVisibleItemPosition() + } else if (layoutManager is StaggeredGridLayoutManager) { + lastItemPosition = + layoutManager.findLastVisibleItemPositions(null).lastOrNull() ?: -1 + } + result = lastItemPosition + } + return result +} + +fun RecyclerView?.isPositionVisible(position: Int): Boolean { + return this?.layoutManager.isPositionVisible(position) +} + +fun RecyclerView.LayoutManager?.isPositionVisible(position: Int): Boolean { + return position >= 0 && position in findFirstVisibleItemPosition()..findLastVisibleItemPosition() +} \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/ScrollPositionConfig.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/ScrollPositionConfig.kt new file mode 100644 index 0000000..ba7734f --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/ScrollPositionConfig.kt @@ -0,0 +1,20 @@ +package com.angcyo.widget.recycler + +import androidx.recyclerview.widget.RecyclerView + +/** + * 记录[RecyclerView]的滚动位置状态 + * Email:angcyo@126.com + * @author angcyo + * @date 2020/03/19 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ + +data class ScrollPositionConfig( + //位置 + var adapterPosition: Int = RecyclerView.NO_POSITION, + + //上下距离 + var left: Int = 0, + var top: Int = 0 +) \ No newline at end of file diff --git a/dslitem/src/main/java/com/angcyo/widget/recycler/StaggeredGridLayoutManagerWrap.kt b/dslitem/src/main/java/com/angcyo/widget/recycler/StaggeredGridLayoutManagerWrap.kt new file mode 100644 index 0000000..1ab57ec --- /dev/null +++ b/dslitem/src/main/java/com/angcyo/widget/recycler/StaggeredGridLayoutManagerWrap.kt @@ -0,0 +1,37 @@ +package com.angcyo.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2020/01/02 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ + +class StaggeredGridLayoutManagerWrap : StaggeredGridLayoutManager { + + constructor(spanCount: Int, orientation: Int) : super(spanCount, orientation) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun onLayoutChildren( + recycler: RecyclerView.Recycler, + state: RecyclerView.State + ) { + try { + super.onLayoutChildren(recycler, state) + } catch (e: Exception) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/dslitem/src/main/res/drawable-anydpi/lib_check_checked.xml b/dslitem/src/main/res/drawable-anydpi/lib_check_checked.xml new file mode 100644 index 0000000..e872814 --- /dev/null +++ b/dslitem/src/main/res/drawable-anydpi/lib_check_checked.xml @@ -0,0 +1,12 @@ + + + + diff --git a/dslitem/src/main/res/drawable-anydpi/lib_check_normal.xml b/dslitem/src/main/res/drawable-anydpi/lib_check_normal.xml new file mode 100644 index 0000000..4b59643 --- /dev/null +++ b/dslitem/src/main/res/drawable-anydpi/lib_check_normal.xml @@ -0,0 +1,9 @@ + + + diff --git a/dslitem/src/main/res/drawable-v21/lib_ripple_shape.xml b/dslitem/src/main/res/drawable-v21/lib_ripple_shape.xml new file mode 100644 index 0000000..209ae95 --- /dev/null +++ b/dslitem/src/main/res/drawable-v21/lib_ripple_shape.xml @@ -0,0 +1,3 @@ + + diff --git a/dslitem/src/main/res/drawable/lib_check_selector.xml b/dslitem/src/main/res/drawable/lib_check_selector.xml new file mode 100644 index 0000000..954992a --- /dev/null +++ b/dslitem/src/main/res/drawable/lib_check_selector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dslitem/src/main/res/layout/dsl_label_edit_item.xml b/dslitem/src/main/res/layout/dsl_label_edit_item.xml index adc4a9f..053848b 100644 --- a/dslitem/src/main/res/layout/dsl_label_edit_item.xml +++ b/dslitem/src/main/res/layout/dsl_label_edit_item.xml @@ -20,18 +20,38 @@ style="@style/ItemEditStyle" android:layout_width="0dp" app:layout_constraintLeft_toRightOf="@id/lib_label_view" - app:layout_constraintRight_toLeftOf="@id/lib_right_ico_view" + app:layout_constraintRight_toLeftOf="@id/lib_right_wrap_layout" tools:hint="请输入..." tools:text="文本" /> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + \ No newline at end of file diff --git a/dslitem/src/main/res/layout/dsl_nested_recycler_item.xml b/dslitem/src/main/res/layout/dsl_nested_recycler_item.xml new file mode 100644 index 0000000..2014faa --- /dev/null +++ b/dslitem/src/main/res/layout/dsl_nested_recycler_item.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/dslitem/src/main/res/layout/layout_check.xml b/dslitem/src/main/res/layout/layout_check.xml new file mode 100644 index 0000000..dd8e909 --- /dev/null +++ b/dslitem/src/main/res/layout/layout_check.xml @@ -0,0 +1,15 @@ + + \ No newline at end of file diff --git a/dslitem/src/main/res/layout/lib_tab_item_layout.xml b/dslitem/src/main/res/layout/lib_tab_item_layout.xml new file mode 100644 index 0000000..54937d4 --- /dev/null +++ b/dslitem/src/main/res/layout/lib_tab_item_layout.xml @@ -0,0 +1,49 @@ + + + + + + + + + + \ No newline at end of file diff --git a/dslitem/src/main/res/values/attr_badge_view.xml b/dslitem/src/main/res/values/attr_badge_view.xml new file mode 100644 index 0000000..4ce3832 --- /dev/null +++ b/dslitem/src/main/res/values/attr_badge_view.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dslitem/src/main/res/values/attr_dsl_recycler_view.xml b/dslitem/src/main/res/values/attr_dsl_recycler_view.xml new file mode 100644 index 0000000..ce5d4a3 --- /dev/null +++ b/dslitem/src/main/res/values/attr_dsl_recycler_view.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/dslitem/src/main/res/values/item_ids.xml b/dslitem/src/main/res/values/item_ids.xml new file mode 100644 index 0000000..23d9c20 --- /dev/null +++ b/dslitem/src/main/res/values/item_ids.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/dslitem/src/main/res/values/item_styles.xml b/dslitem/src/main/res/values/item_styles.xml index 9b60a4c..5653926 100644 --- a/dslitem/src/main/res/values/item_styles.xml +++ b/dslitem/src/main/res/values/item_styles.xml @@ -1,7 +1,20 @@ - + 75dp + 2dp + 1dp + + + 50dp + + 20dp + + @dimen/text_assist_size + + 50dp + + #C3C7CF + + + + + + \ No newline at end of file diff --git a/dslitem2/.gitignore b/dslitem2/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/dslitem2/.gitignore @@ -0,0 +1 @@ +/build diff --git a/dslitem2/README.md b/dslitem2/README.md new file mode 100644 index 0000000..8aa1ff6 --- /dev/null +++ b/dslitem2/README.md @@ -0,0 +1,16 @@ +# dslitem +2020-01-01 + +常用 `DslItem`, 如果此库丰富了, 那么UI, 就是`乐高`拼出来的. + +```kotlin +renderDslAdapter{ + dslItem1() + dslItem2() + dslItem3() + ... + dslItemX() + dslItemY() + dslItemZ() +} +``` \ No newline at end of file diff --git a/dslitem2/build.gradle b/dslitem2/build.gradle new file mode 100644 index 0000000..1e75885 --- /dev/null +++ b/dslitem2/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk Integer.parseInt(C_SDK) + + defaultConfig { + vectorDrawables.useSupportLibrary = true + + minSdk Integer.parseInt(M_SDK) + targetSdk Integer.parseInt(T_SDK) + + consumerProguardFiles 'consumer-rules.pro' + } + namespace 'com.angcyo.item2' +} + +dependencies { + api project(":dslitem") + + //https://jcenter.bintray.com/rouchuan/viewpagerlayoutmanager/viewpagerlayoutmanager/ + //https://github.com/leochuan/ViewPagerLayoutManager + //https://github.com/angcyo/ViewPagerLayoutManager + //api 'rouchuan.viewpagerlayoutmanager:viewpagerlayoutmanager:2.0.22' + api 'com.github.angcyo:ViewPagerLayoutManager:2.1.3' +} diff --git a/dslitem2/consumer-rules.pro b/dslitem2/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/dslitem2/proguard-rules.pro b/dslitem2/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/dslitem2/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set 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 diff --git a/dslitem2/src/main/AndroidManifest.xml b/dslitem2/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a90f2ae --- /dev/null +++ b/dslitem2/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/dslitem2/src/main/java/com/angcyo/item2/DslBannerItem.kt b/dslitem2/src/main/java/com/angcyo/item2/DslBannerItem.kt new file mode 100644 index 0000000..2de5554 --- /dev/null +++ b/dslitem2/src/main/java/com/angcyo/item2/DslBannerItem.kt @@ -0,0 +1,87 @@ +package com.angcyo.item2 + +import androidx.recyclerview.widget.RecyclerView +import com.angcyo.dsladapter.DslAdapterItem +import com.angcyo.dsladapter.DslViewHolder +import com.angcyo.item.DslNestedRecyclerItem +import com.angcyo.item.base.LibInitProvider +import com.angcyo.item2.widget.recycler.DrawableIndicator +import com.leochuan.ScaleLayoutManager +import com.leochuan.ViewPagerLayoutManager + +/** + * 轮播图切换item + * Email:angcyo@126.com + * @author angcyo + * @date 2020/03/18 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +open class DslBannerItem : DslNestedRecyclerItem() { + + init { + itemLayoutId = R.layout.dsl_banner_item + + nestedRecyclerItemConfig.itemNestedLayoutManager = + ScaleLayoutManager(LibInitProvider.contentProvider, 0).apply { + recycleChildrenOnDetach = true + isFullItem = true //全屏item + maxVisibleItemCount = 3 //最大可见item数量 + infinite = true //无限滚动 + itemSpace = 0 //item之间的间隙 + minScale = 1f + maxScale = 1f + maxAlpha = 1f + minAlpha = 1f + } + } + + val pagerLayoutManager: ViewPagerLayoutManager? + get() = nestedRecyclerItemConfig.itemNestedLayoutManager as? ViewPagerLayoutManager + + override fun onBindNestedRecyclerView( + recyclerView: RecyclerView, + itemHolder: DslViewHolder, + itemPosition: Int, + adapterItem: DslAdapterItem, + payloads: List + ) { + super.onBindNestedRecyclerView( + recyclerView, + itemHolder, + itemPosition, + adapterItem, + payloads + ) + + val drawableIndicator: DrawableIndicator? = itemHolder.v(R.id.lib_drawable_indicator) + + //page切换监听 + pagerLayoutManager?.setOnPageChangeListener(object : + ViewPagerLayoutManager.OnPageChangeListener { + override fun onPageScrollStateChanged(state: Int) { + //L.v(state) + } + + override fun onPageSelected(position: Int) { + //L.v(position), 相当页面滑动也会通知. + nestedRecyclerItemConfig._scrollPositionConfig?.adapterPosition = position + drawableIndicator?.animatorToIndex(position) + } + }) + + //列表 + recyclerView.apply { + drawableIndicator?.indicatorCount = nestedRecyclerItemConfig.itemNestedAdapter.itemCount + + nestedRecyclerItemConfig.itemNestedAdapter.onDispatchUpdatesOnce { + drawableIndicator?.indicatorCount = it.itemCount + } + + if (nestedRecyclerItemConfig.itemKeepScrollPosition) { + nestedRecyclerItemConfig._scrollPositionConfig?.run { + scrollToPosition(adapterPosition) + } + } + } + } +} \ No newline at end of file diff --git a/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/DrawableIndicator.kt b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/DrawableIndicator.kt new file mode 100644 index 0000000..6cf102c --- /dev/null +++ b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/DrawableIndicator.kt @@ -0,0 +1,152 @@ +package com.angcyo.item2.widget.recycler + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import com.angcyo.dpi +import com.angcyo.item.anim +import com.angcyo.item.drawHeight +import com.angcyo.item.getMode +import com.angcyo.item.getSize +import com.angcyo.item.loadDrawable +import com.angcyo.item2.R +import kotlin.math.max + +/** + * 2种Drawable切换的指示器 + * Email:angcyo@126.com + * @author angcyo + * @date 2020/03/18 + */ +class DrawableIndicator(context: Context, attributeSet: AttributeSet? = null) : + View(context, attributeSet) { + + /**影响宽高*/ + var drawableSize: Int by RequestLayoutProperty(6 * dpi) + + /**影响宽度*/ + var drawableSizeFocus: Int by RequestLayoutProperty(12 * dpi) + + var indicatorDrawable: Drawable? = loadDrawable(R.drawable.lib_indicator_normal) + var indicatorDrawableFocus: Drawable? = loadDrawable(R.drawable.lib_indicator_focus) + + /**指示器数量*/ + var indicatorCount: Int by RequestLayoutProperty(0) + + /**当前显示*/ + var indicatorIndex: Int by InvalidateProperty(0) + + /**间隙*/ + var indicatorSpace: Int by RequestLayoutProperty(4 * dpi) + + init { + val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DrawableIndicator) + + if (typedArray.hasValue(R.styleable.DrawableIndicator_r_indicator_drawable)) { + indicatorDrawable = + typedArray.getDrawable(R.styleable.DrawableIndicator_r_indicator_drawable) + } + if (typedArray.hasValue(R.styleable.DrawableIndicator_r_indicator_drawable_focus)) { + indicatorDrawableFocus = + typedArray.getDrawable(R.styleable.DrawableIndicator_r_indicator_drawable_focus) + } + + drawableSize = typedArray.getDimensionPixelOffset( + R.styleable.DrawableIndicator_r_indicator_size, + drawableSize + ) + drawableSizeFocus = typedArray.getDimensionPixelOffset( + R.styleable.DrawableIndicator_r_indicator_size_focus, + drawableSizeFocus + ) + indicatorSpace = typedArray.getDimensionPixelOffset( + R.styleable.DrawableIndicator_r_indicator_space, + indicatorSpace + ) + typedArray.recycle() + + if (isInEditMode) { + indicatorCount = 5 + indicatorIndex = 2 + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + var widthSize = widthMeasureSpec.getSize() + var heightSize = heightMeasureSpec.getSize() + if (widthMeasureSpec.getMode() != MeasureSpec.EXACTLY) { + widthSize = paddingLeft + paddingRight + + drawableSize * (indicatorCount - 1) + drawableSizeFocus + + indicatorSpace * (indicatorCount - 1) + } + if (heightMeasureSpec.getMode() != MeasureSpec.EXACTLY) { + heightSize = paddingTop + paddingBottom + max(drawableSize, drawableSizeFocus) + } + //super.onMeasure(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension( + max(widthSize, suggestedMinimumWidth), + max(heightSize, suggestedMinimumHeight) + ) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (indicatorCount > 1) { + //大于1个时, 才绘制 + var left = paddingLeft + var top = paddingTop + for (i in 0 until indicatorCount) { + val width: Int = when { + _animator?.isStarted == true && i == indicatorIndex -> (drawableSize - (drawableSize - drawableSizeFocus) * _animatorFraction).toInt() + _animator?.isStarted == true && i == _animatorFromIndex -> (drawableSizeFocus - (drawableSizeFocus - drawableSize) * _animatorFraction).toInt() + i == indicatorIndex -> drawableSizeFocus + else -> drawableSize + } + + val height = drawableSize + + top = paddingTop + (drawHeight - height) / 2 + + val drawable = if (i == indicatorIndex) { + indicatorDrawableFocus + } else { + indicatorDrawable + } + drawable?.apply { + val right = left + width + setBounds(left, top, right, top + height) + left = right + indicatorSpace + draw(canvas) + } + } + } + } + + var _animator: ValueAnimator? = null + var _animatorFromIndex: Int = -1 + var _animatorFraction: Float = 1f + + /**使用动画的方式切换*/ + fun animatorToIndex(index: Int) { + _animator?.cancel() + _animator = null + + _animatorFromIndex = indicatorIndex + indicatorIndex = index + + if (_animatorFromIndex == indicatorIndex) { + return + } + + _animator = anim(0f, 1f) { + onAnimatorUpdateValue = { value, fraction -> + _animatorFraction = fraction + postInvalidateOnAnimation() + } + } + } +} \ No newline at end of file diff --git a/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/InvalidateProperty.kt b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/InvalidateProperty.kt new file mode 100644 index 0000000..6461de4 --- /dev/null +++ b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/InvalidateProperty.kt @@ -0,0 +1,33 @@ +package com.angcyo.item2.widget.recycler + +import android.graphics.drawable.Drawable +import android.view.View +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * 属性改变时, 自动触发[android.view.View.invalidate] + * @author angcyo + * @since 2022/08/06 + */ +class InvalidateProperty(var value: VALUE) : ReadWriteProperty { + + override fun getValue(thisRef: VIEW, property: KProperty<*>): VALUE { + val v = value + if (v is Drawable) { + if (v.callback != thisRef) { + v.callback = thisRef + } + } + return v + } + + override fun setValue(thisRef: VIEW, property: KProperty<*>, value: VALUE) { + val old = this.value + this.value = value + if (old != value) { + thisRef.invalidate() + } + } + +} \ No newline at end of file diff --git a/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/LoopRecyclerView.kt b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/LoopRecyclerView.kt new file mode 100644 index 0000000..66f9303 --- /dev/null +++ b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/LoopRecyclerView.kt @@ -0,0 +1,132 @@ +package com.angcyo.item2.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.angcyo.github.widget.recycler.LoopSnapHelper +import com.angcyo.item2.R +import com.angcyo.widget.recycler.DslRecyclerView +import com.leochuan.AutoPlaySnapHelper + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2020/03/18 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +open class LoopRecyclerView(context: Context, attributeSet: AttributeSet? = null) : + DslRecyclerView(context, attributeSet) { + + /**是否自动开始循环滚动*/ + var autoStartLoop: Boolean = true + + /**循环滚动助手*/ + var loopSnapHelper: LoopSnapHelper = + LoopSnapHelper(AutoPlaySnapHelper.TIME_INTERVAL, AutoPlaySnapHelper.RIGHT) + + /**一个一个滑动*/ + var snapByOne: Boolean = true + + val enableLoop: Boolean + get() = (adapter?.itemCount ?: 0) > 1 + + init { + val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.LoopRecyclerView) + val timeInterval = typedArray.getInt( + R.styleable.LoopRecyclerView_r_loop_interval, + 3000 + ) + val direction = + typedArray.getInt( + R.styleable.LoopRecyclerView_r_loop_direction, + AutoPlaySnapHelper.RIGHT + ) + autoStartLoop = + typedArray.getBoolean(R.styleable.LoopRecyclerView_r_auto_start, autoStartLoop) + + snapByOne = typedArray.getBoolean(R.styleable.LoopRecyclerView_r_snap_by_one, snapByOne) + + loopSnapHelper.loopDuration = typedArray.getInt( + R.styleable.LoopRecyclerView_r_loop_duration, + loopSnapHelper.loopDuration + ) + + typedArray.recycle() + + loopSnapHelper.setTimeInterval(timeInterval) + loopSnapHelper.setDirection(direction) + loopSnapHelper.snapScrollOne = snapByOne + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + _startInner() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + pause() + } + + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + if (visibility == View.VISIBLE) { + _startInner() + } else { + pause() + } + } + + override fun setLayoutManager(layout: LayoutManager?) { + super.setLayoutManager(layout) + + if (!isInEditMode) { + loopSnapHelper.detachFromRecyclerView() + //如果在super的构造方法里面调用了setLayoutManager, 此时[loopSnapHelper]还未初始化, 就会报空指针 + loopSnapHelper.attachToRecyclerView(this) //会自动开启无线循环 + _startInner() + } + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + val result = super.dispatchTouchEvent(ev) + when (ev.action) { + MotionEvent.ACTION_DOWN -> pause() + MotionEvent.ACTION_UP -> _startInner() + } + return result + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + return super.onTouchEvent(ev) + } + + override fun scrollBy(x: Int, y: Int) { + super.scrollBy(x, y) + } + + override fun scrollTo(x: Int, y: Int) { + super.scrollTo(x, y) + } + + fun _startInner() { + if (autoStartLoop) { + start() + } + } + + open fun start() { + if (enableLoop) { + //大于一个时才滚动 + loopSnapHelper.start() + } else { + pause() + } + } + + open fun pause() { + loopSnapHelper.pause() + } +} diff --git a/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/LoopSnapHelper.kt b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/LoopSnapHelper.kt new file mode 100644 index 0000000..53e325a --- /dev/null +++ b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/LoopSnapHelper.kt @@ -0,0 +1,95 @@ +package com.angcyo.github.widget.recycler + +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import androidx.recyclerview.widget.RecyclerView +import com.angcyo.dsladapter.DslAdapter +import com.angcyo.github.dslitem.ILoopAdapterItem +import com.leochuan.AutoPlaySnapHelper +import com.leochuan.ViewPagerLayoutManager + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2020/03/18 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +class LoopSnapHelper(val interval: Int, direction: Int) : + AutoPlaySnapHelper(interval, direction) { + + var loopInterpolator: Interpolator? = DecelerateInterpolator() + + var loopDuration: Int = 1000//UNDEFINED_DURATION + + /**替换默认的时间间隔*/ + fun hookInterval() { + if (mRecyclerView.adapter is DslAdapter && mRecyclerView.layoutManager is ViewPagerLayoutManager) { + val layoutManager: ViewPagerLayoutManager = + mRecyclerView.layoutManager as ViewPagerLayoutManager + + val currentPosition = + layoutManager.currentPosition * if (layoutManager.reverseLayout) -1 else 1 + + (mRecyclerView.adapter as DslAdapter).getItemData(currentPosition, true)?.apply { + if (this is ILoopAdapterItem) { + //替换默认的时间间隔 + setTimeInterval(this.getLoopInterval()) + } + } + } + } + + /**恢复默认的时间间隔*/ + fun restoreInterval() { + //恢复默认的时间间隔 + setTimeInterval(interval) + } + + override fun onRun(layoutManager: ViewPagerLayoutManager) { + if (mRecyclerView?.adapter?.itemCount ?: 0 <= 0) { + return + } + hookInterval() + + val currentPosition = + layoutManager.currentPositionOffset * if (layoutManager.reverseLayout) -1 else 1 + + val targetPosition = if (direction == RIGHT) currentPosition + 1 else currentPosition - 1 + + val delta: Int = layoutManager.getOffsetToPosition(targetPosition) + if (layoutManager.orientation == RecyclerView.VERTICAL) { + mRecyclerView?.smoothScrollBy(0, delta, loopInterpolator, loopDuration) + } else { + mRecyclerView?.smoothScrollBy(delta, 0, loopInterpolator, loopDuration) + } + + handler.postDelayed(autoPlayRunnable, timeInterval.toLong()) + + restoreInterval() + } + + override fun start() { + if (mRecyclerView == null) { + return + } + + if (mRecyclerView.layoutManager == null) { + return + } + + if (mRecyclerView.layoutManager !is ViewPagerLayoutManager) { + return + } + hookInterval() + super.start() + restoreInterval() + } + + fun detachFromRecyclerView() { + if (mRecyclerView != null) { + destroyCallbacks() + } + mRecyclerView = null + } +} \ No newline at end of file diff --git a/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/RequestLayoutProperty.kt b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/RequestLayoutProperty.kt new file mode 100644 index 0000000..cc38d0c --- /dev/null +++ b/dslitem2/src/main/java/com/angcyo/item2/widget/recycler/RequestLayoutProperty.kt @@ -0,0 +1,30 @@ +package com.angcyo.item2.widget.recycler + +import android.view.View +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * + * Email:angcyo@126.com + * @author angcyo + * @date 2019/12/24 + * Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved. + */ +class RequestLayoutProperty(var value: T) : ReadWriteProperty { + override fun getValue(thisRef: View, property: KProperty<*>): T = value + + override fun setValue(thisRef: View, property: KProperty<*>, value: T) { + this.value = value + thisRef.requestLayout() + } +} + +class RequestLayoutAnyProperty(var value: T, val view: View?) : ReadWriteProperty { + override fun getValue(thisRef: Any, property: KProperty<*>): T = value + + override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + this.value = value + view?.requestLayout() + } +} \ No newline at end of file diff --git a/dslitem2/src/main/res/drawable/lib_indicator_focus.xml b/dslitem2/src/main/res/drawable/lib_indicator_focus.xml new file mode 100644 index 0000000..9692ac7 --- /dev/null +++ b/dslitem2/src/main/res/drawable/lib_indicator_focus.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/dslitem2/src/main/res/drawable/lib_indicator_normal.xml b/dslitem2/src/main/res/drawable/lib_indicator_normal.xml new file mode 100644 index 0000000..31b39fa --- /dev/null +++ b/dslitem2/src/main/res/drawable/lib_indicator_normal.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/dslitem2/src/main/res/layout/dsl_banner_item.xml b/dslitem2/src/main/res/layout/dsl_banner_item.xml new file mode 100644 index 0000000..6b55c39 --- /dev/null +++ b/dslitem2/src/main/res/layout/dsl_banner_item.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/dslitem2/src/main/res/values/attr_drawable_indicator.xml b/dslitem2/src/main/res/values/attr_drawable_indicator.xml new file mode 100644 index 0000000..0a482b4 --- /dev/null +++ b/dslitem2/src/main/res/values/attr_drawable_indicator.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/dslitem2/src/main/res/values/attr_loop_recycler_view.xml b/dslitem2/src/main/res/values/attr_loop_recycler_view.xml new file mode 100644 index 0000000..c407ac1 --- /dev/null +++ b/dslitem2/src/main/res/values/attr_loop_recycler_view.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dslitem2/src/main/res/values/item2_colors.xml b/dslitem2/src/main/res/values/item2_colors.xml new file mode 100644 index 0000000..048b392 --- /dev/null +++ b/dslitem2/src/main/res/values/item2_colors.xml @@ -0,0 +1,5 @@ + + + #4D727272 + @color/colorAccent + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 9e73ebc..05ff4a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,3 +22,7 @@ kotlin.code.style=official android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false + +M_SDK=18 +T_SDK=33 +C_SDK=33 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 151dd39..a4756fc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name='DslItem' include ':demo' include ':dslitem' +include ':dslitem2'