Compare commits

...

9 Commits

Author SHA1 Message Date
a0982360ec 调整了点复制按钮 2024-08-28 17:53:57 +08:00
de339d31f7 新增了分类
然后不知道改了啥
2024-08-28 17:52:31 +08:00
9e889a2f14 update 2024-08-02 10:34:16 +08:00
d305c7809c 正式版1.2版
优化:列表新增分割线
优化:替换清空按钮
优化:修改指纹认证流程
2021-08-16 09:38:28 +08:00
ac900e5647 正式版1.1版
新增备注功能
数据库更新
2021-06-30 20:51:48 +08:00
e12cd888b5 正式版1.0版
新增icon
新增指纹/PIN码验证启动
2021-02-22 23:05:34 +08:00
yutou
9057f8b765 update 2021-02-22 18:34:56 +08:00
yutou
0c5bebf60a update 2021-02-20 18:29:46 +08:00
yutou
0886fe1feb init 2021-02-19 14:48:27 +08:00
116 changed files with 9657 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

1
TabLayout/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

27
TabLayout/build.gradle Normal file
View File

@ -0,0 +1,27 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion 28
targetSdkVersion 33
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
namespace 'com.angcyo.tablayout'
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
}
//apply from: "$gradleHost/master/publish.gradle"

View File

21
TabLayout/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@ -0,0 +1,200 @@
package com.angcyo.tablayout
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.*
import android.graphics.drawable.Drawable
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import androidx.core.view.ViewCompat
/**
* 基础自绘Drawable
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/25
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
abstract class AbsDslDrawable : Drawable() {
companion object {
/**不绘制*/
const val DRAW_TYPE_DRAW_NONE = 0x00
/**[android.view.View.draw]*/
const val DRAW_TYPE_DRAW_AFTER = 0x01
const val DRAW_TYPE_DRAW_BEFORE = 0x02
/**[android.view.View.onDraw]*/
const val DRAW_TYPE_ON_DRAW_AFTER = 0x04
const val DRAW_TYPE_ON_DRAW_BEFORE = 0x08
}
/**画笔*/
val textPaint: TextPaint by lazy {
TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
isFilterBitmap = true
style = Paint.Style.FILL
textSize = 12 * dp
}
}
val drawRect = Rect()
val drawRectF = RectF()
/**需要在那个方法中触发绘制*/
var drawType = DRAW_TYPE_ON_DRAW_AFTER
/**xml属性读取*/
open fun initAttribute(
context: Context,
attributeSet: AttributeSet? = null
) {
//val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.xxx)
//typedArray.recycle()
}
//<editor-fold desc="View相关方法">
/**附着的[View]*/
val attachView: View?
get() = if (callback is View) callback as? View else null
val isInEditMode: Boolean
get() = attachView?.isInEditMode ?: false
val paddingLeft: Int
get() = attachView?.paddingLeft ?: 0
val paddingRight: Int
get() = attachView?.paddingRight ?: 0
val paddingTop: Int
get() = attachView?.paddingTop ?: 0
val paddingBottom: Int
get() = attachView?.paddingBottom ?: 0
val viewHeight: Int
get() = attachView?.measuredHeight ?: 0
val viewWidth: Int
get() = attachView?.measuredWidth ?: 0
val viewDrawHeight: Int
get() = viewHeight - paddingTop - paddingBottom
val viewDrawWidth: Int
get() = viewWidth - paddingLeft - paddingRight
val isViewRtl: Boolean
get() = attachView != null && ViewCompat.getLayoutDirection(attachView!!) == ViewCompat.LAYOUT_DIRECTION_RTL
//</editor-fold desc="View相关方法">
//<editor-fold desc="基类方法">
/**核心方法, 绘制*/
override fun draw(canvas: Canvas) {
}
override fun getIntrinsicWidth(): Int {
return super.getIntrinsicWidth()
}
override fun getMinimumWidth(): Int {
return super.getMinimumWidth()
}
override fun getIntrinsicHeight(): Int {
return super.getIntrinsicHeight()
}
override fun getMinimumHeight(): Int {
return super.getMinimumHeight()
}
override fun setAlpha(alpha: Int) {
if (textPaint.alpha != alpha) {
textPaint.alpha = alpha
invalidateSelf()
}
}
override fun getAlpha(): Int {
return textPaint.alpha
}
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
super.setBounds(left, top, right, bottom)
}
override fun setBounds(bounds: Rect) {
super.setBounds(bounds)
}
//不透明度
override fun getOpacity(): Int {
return if (alpha < 255) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE
}
override fun getColorFilter(): ColorFilter? {
return textPaint.colorFilter
}
override fun setColorFilter(colorFilter: ColorFilter?) {
textPaint.colorFilter = colorFilter
invalidateSelf()
}
override fun mutate(): Drawable {
return super.mutate()
}
override fun setDither(dither: Boolean) {
textPaint.isDither = dither
invalidateSelf()
}
override fun setFilterBitmap(filter: Boolean) {
textPaint.isFilterBitmap = filter
invalidateSelf()
}
override fun isFilterBitmap(): Boolean {
return textPaint.isFilterBitmap
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
}
override fun onLevelChange(level: Int): Boolean {
return super.onLevelChange(level)
}
override fun onStateChange(state: IntArray): Boolean {
return super.onStateChange(state)
}
override fun setTintList(tint: ColorStateList?) {
super.setTintList(tint)
}
override fun setTintMode(tintMode: PorterDuff.Mode?) {
super.setTintMode(tintMode)
}
override fun setTintBlendMode(blendMode: BlendMode?) {
super.setTintBlendMode(blendMode)
}
override fun setHotspot(x: Float, y: Float) {
super.setHotspot(x, y)
}
override fun setHotspotBounds(left: Int, top: Int, right: Int, bottom: Int) {
super.setHotspotBounds(left, top, right, bottom)
}
override fun getHotspotBounds(outRect: Rect) {
super.getHotspotBounds(outRect)
}
//</editor-fold desc="基类方法">
}

View File

@ -0,0 +1,275 @@
package com.angcyo.tablayout
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 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
/**原点状态下, 单独配置的偏移*/
var badgeCircleOffsetX: Int = 0
var badgeCircleOffsetY: Int = 0
/**额外偏移距离, 会根据[Gravity]自动取负值*/
var badgeOffsetX: Int = 0
var badgeOffsetY: Int = 0
/**文本偏移*/
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
gravityOffsetY = badgeCircleOffsetY
} else {
gravityOffsetX = badgeOffsetX
gravityOffsetY = badgeOffsetY
}
val textWidth = textPaint.textWidth(badgeText)
val textHeight = textPaint.textHeight()
val drawHeight = if (isCircle) {
badgeCircleRadius.toFloat()
} else {
val height = textHeight + badgePaddingTop + badgePaddingBottom
if (badgeMinHeight > 0) {
max(height, badgeMinHeight.toFloat())
} else {
height
}
}
val drawWidth = if (isCircle) {
badgeCircleRadius.toFloat()
} 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.isCenter()) {
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
val bgTop = _gravityTop
//绘制背景
if (badgeAutoCircle && badgeText?.length == 1) {
if (gradientSolidColor != Color.TRANSPARENT) {
textPaint.color = gradientSolidColor
canvas.drawCircle(
centerX.toFloat(),
centerY.toFloat(),
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
} else if (badgeAutoCircle && badgeText?.length == 1) {
max(maxWidth, maxHeight)
} else {
maxWidth
}
return max(badgeMinWidth, width)
}
override fun getIntrinsicHeight(): Int {
val height = if (isCircle) {
badgeCircleRadius * 2
} else if (badgeAutoCircle && badgeText?.length == 1) {
max(maxWidth, maxHeight)
} else {
maxHeight
}
return max(badgeMinHeight, height)
}
}

View File

@ -0,0 +1,306 @@
package com.angcyo.tablayout
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Build
import androidx.annotation.IntDef
import java.util.*
/**
* 用来构建GradientDrawable
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/27
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
open class DslGradientDrawable : AbsDslDrawable() {
/**形状*/
@Shape
var gradientShape = GradientDrawable.RECTANGLE
/**填充的颜色*/
var gradientSolidColor = Color.TRANSPARENT
/**边框的颜色*/
var gradientStrokeColor = Color.TRANSPARENT
/**边框的宽度*/
var gradientStrokeWidth = 0
/**蚂蚁线的宽度*/
var gradientDashWidth = 0f
/**蚂蚁线之间的间距*/
var gradientDashGap = 0f
/**
* 四个角, 8个设置点的圆角信息
* 左上y轴->左上x轴->右上x轴->右上y轴..., 开始设置.
*/
var gradientRadii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
/**颜色渐变*/
var gradientColors: IntArray? = null
var gradientColorsOffsets: FloatArray? = null
/**渐变中心点坐标*/
var gradientCenterX = 0.5f
var gradientCenterY = 0.5f
/**渐变半径, 非比例值, 是px值. [GradientDrawable.RADIAL_GRADIENT]类型才有效*/
var gradientRadius = 0.5f
/** 渐变方向, 默认从左到右 */
var gradientOrientation = GradientDrawable.Orientation.LEFT_RIGHT
/** 渐变类型 */
@GradientType
var gradientType = GradientDrawable.LINEAR_GRADIENT
/**真正绘制的[Drawable]*/
var originDrawable: Drawable? = null
/**宽度补偿*/
var gradientWidthOffset: Int = 0
/**高度补偿*/
var gradientHeightOffset: Int = 0
/**当前的配置, 是否能生成有效的[GradientDrawable]*/
open fun isValidConfig(): Boolean {
return gradientSolidColor != Color.TRANSPARENT ||
gradientStrokeColor != Color.TRANSPARENT ||
gradientColors != null
}
fun _fillRadii(array: FloatArray, radii: String?) {
if (radii.isNullOrEmpty()) {
return
}
val split = radii.split(",")
if (split.size != 8) {
throw IllegalArgumentException("radii 需要8个值.")
} else {
val dp = Resources.getSystem().displayMetrics.density
for (i in split.indices) {
array[i] = split[i].toFloat() * dp
}
}
}
fun fillRadii(radius: Float) {
Arrays.fill(gradientRadii, radius)
}
fun fillRadii(radius: Int) {
_fillRadii(gradientRadii, radius.toFloat())
}
fun _fillRadii(array: FloatArray, radius: Float) {
Arrays.fill(array, radius)
}
fun _fillRadii(array: FloatArray, radius: Int) {
_fillRadii(array, radius.toFloat())
}
fun _fillColor(colors: String?): IntArray? {
if (colors.isNullOrEmpty()) {
return null
}
val split = colors.split(",")
return IntArray(split.size) {
val str = split[it]
if (str.startsWith("#")) {
Color.parseColor(str)
} else {
str.toInt()
}
}
}
/**构建或者更新[originDrawable]*/
open fun updateOriginDrawable(): GradientDrawable? {
val drawable: GradientDrawable? = when (originDrawable) {
null -> GradientDrawable()
is GradientDrawable -> originDrawable as GradientDrawable
else -> {
null
}
}
drawable?.apply {
bounds = this@DslGradientDrawable.bounds
shape = gradientShape
setStroke(
gradientStrokeWidth,
gradientStrokeColor,
gradientDashWidth,
gradientDashGap
)
setColor(gradientSolidColor)
cornerRadii = gradientRadii
if (gradientColors != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setGradientCenter(
this@DslGradientDrawable.gradientCenterX,
this@DslGradientDrawable.gradientCenterY
)
}
gradientRadius = this@DslGradientDrawable.gradientRadius
gradientType = this@DslGradientDrawable.gradientType
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
orientation = gradientOrientation
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setColors(gradientColors, gradientColorsOffsets)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
colors = gradientColors
}
}
originDrawable = this
invalidateSelf()
}
return drawable
}
open fun configDrawable(config: DslGradientDrawable.() -> Unit): DslGradientDrawable {
this.config()
updateOriginDrawable()
return this
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
originDrawable?.apply {
setBounds(
this@DslGradientDrawable.bounds.left - gradientWidthOffset / 2,
this@DslGradientDrawable.bounds.top - gradientHeightOffset / 2,
this@DslGradientDrawable.bounds.right + gradientWidthOffset / 2,
this@DslGradientDrawable.bounds.bottom + gradientHeightOffset / 2
)
draw(canvas)
}
}
//<editor-fold desc="圆角相关配置">
/**
* 4个角, 8个点 圆角配置
*/
fun cornerRadii(radii: FloatArray) {
gradientRadii = radii
}
fun cornerRadius(radii: Float) {
Arrays.fill(gradientRadii, radii)
}
fun cornerRadius(
leftTop: Float = 0f,
rightTop: Float = 0f,
rightBottom: Float = 0f,
leftBottom: Float = 0f
) {
gradientRadii[0] = leftTop
gradientRadii[1] = leftTop
gradientRadii[2] = rightTop
gradientRadii[3] = rightTop
gradientRadii[4] = rightBottom
gradientRadii[5] = rightBottom
gradientRadii[6] = leftBottom
gradientRadii[7] = leftBottom
}
/**
* 只配置左边的圆角
*/
fun cornerRadiiLeft(radii: Float) {
gradientRadii[0] = radii
gradientRadii[1] = radii
gradientRadii[6] = radii
gradientRadii[7] = radii
}
fun cornerRadiiRight(radii: Float) {
gradientRadii[2] = radii
gradientRadii[3] = radii
gradientRadii[4] = radii
gradientRadii[5] = radii
}
fun cornerRadiiTop(radii: Float) {
gradientRadii[0] = radii
gradientRadii[1] = radii
gradientRadii[2] = radii
gradientRadii[3] = radii
}
fun cornerRadiiBottom(radii: Float) {
gradientRadii[4] = radii
gradientRadii[5] = radii
gradientRadii[6] = radii
gradientRadii[7] = radii
}
//</editor-fold desc="圆角相关配置">
//<editor-fold desc="传递属性">
override fun setColorFilter(colorFilter: ColorFilter?) {
super.setColorFilter(colorFilter)
originDrawable?.colorFilter = colorFilter
}
override fun setTintList(tint: ColorStateList?) {
super.setTintList(tint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
originDrawable?.setTintList(tint)
}
}
override fun setState(stateSet: IntArray): Boolean {
return originDrawable?.setState(stateSet) ?: super.setState(stateSet)
}
override fun getState(): IntArray {
return originDrawable?.state ?: super.getState()
}
//</editor-fold desc="传递属性">
}
@IntDef(
GradientDrawable.RECTANGLE,
GradientDrawable.OVAL,
GradientDrawable.LINE,
GradientDrawable.RING
)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
annotation class Shape
@IntDef(
GradientDrawable.LINEAR_GRADIENT,
GradientDrawable.RADIAL_GRADIENT,
GradientDrawable.SWEEP_GRADIENT
)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
annotation class GradientType
/**快速创建[GradientDrawable]*/
fun dslGradientDrawable(action: DslGradientDrawable.() -> Unit): GradientDrawable {
return DslGradientDrawable().run {
action()
updateOriginDrawable()!!
}
}

View File

@ -0,0 +1,215 @@
package com.angcyo.tablayout
import android.graphics.Rect
import android.graphics.RectF
import android.view.Gravity
/**
* [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
/**使用中心坐标作为定位参考, 否则就是四条边*/
var gravityRelativeCenter: Boolean = true
/**额外偏移距离, 会根据[Gravity]自动取负值*/
var gravityOffsetX: Int = 0
var gravityOffsetY: Int = 0
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 = 0
var _gravityTop = 0
var _gravityRight = 0
var _gravityBottom = 0
//根据gravity调整后的offset
var _gravityOffsetX = 0
var _gravityOffsetY = 0
/**根据[gravity]返回在[gravityBounds]中的[left] [top]位置*/
fun applyGravity(
width: Float = _targetWidth,
height: Float = _targetHeight,
callback: (centerX: Int, centerY: Int) -> Unit
) {
_targetWidth = width
_targetHeight = height
val layoutDirection = 0
val absoluteGravity = Gravity.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).toInt()
Gravity.RIGHT -> (gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
Gravity.END -> (gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
else -> (gravityBounds.left + _gravityOffsetX + if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
}
val centerY = when (verticalGravity) {
Gravity.CENTER_VERTICAL -> (gravityBounds.top + gravityBounds.height() / 2 + _gravityOffsetY).toInt()
Gravity.BOTTOM -> (gravityBounds.bottom + _gravityOffsetY - if (gravityRelativeCenter) 0f else _targetHeight / 2).toInt()
else -> (gravityBounds.top + _gravityOffsetY + if (gravityRelativeCenter) 0f else _targetHeight / 2).toInt()
}
//缓存
_horizontalGravity = horizontalGravity
_verticalGravity = verticalGravity
_isCenterGravity = horizontalGravity == Gravity.CENTER_HORIZONTAL &&
verticalGravity == Gravity.CENTER_VERTICAL
_gravityLeft = (centerX - _targetWidth / 2).toInt()
_gravityRight = (centerX + _targetWidth / 2).toInt()
_gravityTop = (centerY - _targetHeight / 2).toInt()
_gravityBottom = (centerY + _targetHeight / 2).toInt()
//回调
callback(centerX, centerY)
}
}
/**
* 默认计算出的是目标中心点坐标参考距离
* [com.angcyo.drawable.DslGravity.getGravityRelativeCenter]
* */
fun dslGravity(
rect: RectF, //计算的矩形
gravity: Int, //重力
width: Float, //放置目标的宽度
height: Float, //放置目标的高度
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
offsetY: Int = 0,
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
): DslGravity {
val _dslGravity = DslGravity()
_dslGravity.setGravityBounds(rect)
_config(_dslGravity, gravity, width, height, offsetX, offsetY, callback)
return _dslGravity
}
/**
* 默认计算出的是目标中心点坐标参考距离
* [com.angcyo.drawable.DslGravity.getGravityRelativeCenter]
* */
fun dslGravity(
rect: Rect, //计算的矩形
gravity: Int, //重力
width: Float, //放置目标的宽度
height: Float, //放置目标的高度
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
offsetY: Int = 0,
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
): DslGravity {
val _dslGravity = DslGravity()
_dslGravity.setGravityBounds(rect)
_config(_dslGravity, gravity, width, height, offsetX, offsetY, callback)
return _dslGravity
}
private fun _config(
_dslGravity: DslGravity,
gravity: Int, //重力
width: Float, //放置目标的宽度
height: Float, //放置目标的高度
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
offsetY: Int = 0,
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
) {
_dslGravity.gravity = gravity
_dslGravity.gravityOffsetX = offsetX
_dslGravity.gravityOffsetY = offsetY
_dslGravity.applyGravity(width, height) { centerX, centerY ->
callback(_dslGravity, centerX, centerY)
}
}
/**居中*/
fun Int.isCenter(): 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
}
fun Int.isLeft(): Boolean {
val layoutDirection = 0
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
return horizontalGravity == Gravity.LEFT
}
fun Int.isRight(): Boolean {
val layoutDirection = 0
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
return horizontalGravity == Gravity.RIGHT
}
fun Int.isTop(): Boolean {
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
return verticalGravity == Gravity.TOP
}
fun Int.isBottom(): Boolean {
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
return verticalGravity == Gravity.BOTTOM
}

View File

@ -0,0 +1,438 @@
package com.angcyo.tablayout
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
/**
* 用来操作[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<View> = mutableListOf()
/**
* 选中的索引列表
* */
val selectorIndexList: MutableList<Int> = mutableListOf()
get() {
field.clear()
visibleViewList.forEachIndexed { index, view ->
if (view.isSe()) {
field.add(index)
}
}
return field
}
/**
* 选中的View列表
* */
val selectorViewList: MutableList<View> = 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<View> {
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<Int>,
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) {
"index out of list.".logi()
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<View>, reselect: Boolean, fromUser: Boolean) -> Unit =
{ _, _, _, _ ->
}
/**
* 选中改变回调
* [onSelectViewChange]
* @param fromIndex 单选模式下有效, 表示之前选中的[View], 在可见性[child]列表中的索引
* */
var onSelectIndexChange: (fromIndex: Int, selectIndexList: List<Int>, reselect: Boolean, fromUser: Boolean) -> Unit =
{ fromIndex, selectList, reselect, fromUser ->
"选择:[$fromIndex]->${selectList} reselect:$reselect fromUser:$fromUser".logi()
}
/**
* 当需要选中[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)
}
}

View File

@ -0,0 +1,222 @@
package com.angcyo.tablayout
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
import androidx.annotation.Px
/**
* 角标
* Email:angcyo@126.com
* @author angcyo
* @date 2019/12/13
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
open class DslTabBadge : DslBadgeDrawable() {
/**角标默认配置项*/
val defaultBadgeConfig = TabBadgeConfig()
/**预览的角标属性*/
var xmlBadgeText: String? = null
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
val typedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
gradientSolidColor =
typedArray.getColor(
R.styleable.DslTabLayout_tab_badge_solid_color,
defaultBadgeConfig.badgeSolidColor
)
defaultBadgeConfig.badgeSolidColor = gradientSolidColor
badgeTextColor =
typedArray.getColor(
R.styleable.DslTabLayout_tab_badge_text_color,
defaultBadgeConfig.badgeTextColor
)
defaultBadgeConfig.badgeTextColor = badgeTextColor
gradientStrokeColor =
typedArray.getColor(
R.styleable.DslTabLayout_tab_badge_stroke_color,
defaultBadgeConfig.badgeStrokeColor
)
defaultBadgeConfig.badgeStrokeColor = gradientStrokeColor
gradientStrokeWidth =
typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_stroke_width,
defaultBadgeConfig.badgeStrokeWidth
)
defaultBadgeConfig.badgeStrokeWidth = gradientStrokeWidth
badgeGravity = typedArray.getInt(
R.styleable.DslTabLayout_tab_badge_gravity,
defaultBadgeConfig.badgeGravity
)
defaultBadgeConfig.badgeGravity = badgeGravity
badgeOffsetX = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_offset_x,
defaultBadgeConfig.badgeOffsetX
)
defaultBadgeConfig.badgeOffsetX = badgeOffsetX
badgeOffsetY = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_offset_y,
defaultBadgeConfig.badgeOffsetY
)
defaultBadgeConfig.badgeOffsetY = badgeOffsetY
badgeCircleOffsetX = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_circle_offset_x,
defaultBadgeConfig.badgeOffsetX
)
defaultBadgeConfig.badgeCircleOffsetX = badgeCircleOffsetX
badgeCircleOffsetY = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_circle_offset_y,
defaultBadgeConfig.badgeOffsetY
)
defaultBadgeConfig.badgeCircleOffsetY = badgeCircleOffsetY
badgeCircleRadius = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_circle_radius,
defaultBadgeConfig.badgeCircleRadius
)
defaultBadgeConfig.badgeCircleRadius = badgeCircleRadius
val badgeRadius = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_radius,
defaultBadgeConfig.badgeRadius
)
cornerRadius(badgeRadius.toFloat())
defaultBadgeConfig.badgeRadius = badgeRadius
badgePaddingLeft = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_padding_left,
defaultBadgeConfig.badgePaddingLeft
)
defaultBadgeConfig.badgePaddingLeft = badgePaddingLeft
badgePaddingRight = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_padding_right,
defaultBadgeConfig.badgePaddingRight
)
defaultBadgeConfig.badgePaddingRight = badgePaddingRight
badgePaddingTop = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_padding_top,
defaultBadgeConfig.badgePaddingTop
)
defaultBadgeConfig.badgePaddingTop = badgePaddingTop
badgePaddingBottom = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_padding_bottom,
defaultBadgeConfig.badgePaddingBottom
)
defaultBadgeConfig.badgePaddingBottom = badgePaddingBottom
xmlBadgeText = typedArray.getString(R.styleable.DslTabLayout_tab_badge_text)
badgeTextSize = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_badge_text_size,
defaultBadgeConfig.badgeTextSize.toInt()
).toFloat()
defaultBadgeConfig.badgeTextSize = badgeTextSize
defaultBadgeConfig.badgeAnchorChildIndex =
typedArray.getInteger(
R.styleable.DslTabLayout_tab_badge_anchor_child_index,
defaultBadgeConfig.badgeAnchorChildIndex
)
defaultBadgeConfig.badgeIgnoreChildPadding = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_badge_ignore_child_padding,
defaultBadgeConfig.badgeIgnoreChildPadding
)
defaultBadgeConfig.badgeMinWidth = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_badge_min_width,
defaultBadgeConfig.badgeMinWidth
)
defaultBadgeConfig.badgeMinHeight = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_badge_min_height,
defaultBadgeConfig.badgeMinHeight
)
typedArray.recycle()
super.initAttribute(context, attributeSet)
}
/**使用指定配置, 更新[DslBadgeDrawable]*/
fun updateBadgeConfig(badgeConfig: TabBadgeConfig) {
gradientSolidColor = badgeConfig.badgeSolidColor
gradientStrokeColor = badgeConfig.badgeStrokeColor
gradientStrokeWidth = badgeConfig.badgeStrokeWidth
badgeTextColor = badgeConfig.badgeTextColor
badgeGravity = badgeConfig.badgeGravity
badgeOffsetX = badgeConfig.badgeOffsetX
badgeOffsetY = badgeConfig.badgeOffsetY
badgeCircleOffsetX = badgeConfig.badgeCircleOffsetX
badgeCircleOffsetY = badgeConfig.badgeCircleOffsetY
badgeCircleRadius = badgeConfig.badgeCircleRadius
badgePaddingLeft = badgeConfig.badgePaddingLeft
badgePaddingRight = badgeConfig.badgePaddingRight
badgePaddingTop = badgeConfig.badgePaddingTop
badgePaddingBottom = badgeConfig.badgePaddingBottom
badgeTextSize = badgeConfig.badgeTextSize
cornerRadius(badgeConfig.badgeRadius.toFloat())
badgeMinHeight = badgeConfig.badgeMinHeight
badgeMinWidth = badgeConfig.badgeMinWidth
badgeText = badgeConfig.badgeText
}
}
/**角标绘制参数配置*/
data class TabBadgeConfig(
/**角标的文本(默认居中绘制文本,暂不支持修改), 空字符串会绘制成小圆点
* null 不绘制角标
* "" 空字符绘制圆点
* 其他 正常绘制
* */
var badgeText: String? = null,
/**重力*/
var badgeGravity: Int = Gravity.CENTER,
/**角标背景颜色*/
var badgeSolidColor: Int = Color.RED,
/**角标边框颜色*/
var badgeStrokeColor: Int = Color.TRANSPARENT,
/**角标边框宽度*/
var badgeStrokeWidth: Int = 0,
/**角标文本颜色*/
var badgeTextColor: Int = Color.WHITE,
/**角标文本字体大小*/
@Px
var badgeTextSize: Float = 12 * dp,
/**圆点状态时的半径大小*/
var badgeCircleRadius: Int = 4 * dpi,
/**圆角大小*/
var badgeRadius: Int = 10 * dpi,
/**额外偏移距离, 会根据[Gravity]自动取负值*/
var badgeOffsetX: Int = 0,
var badgeOffsetY: Int = 0,
var badgeCircleOffsetX: Int = 0,
var badgeCircleOffsetY: Int = 0,
/**圆点状态时无效*/
var badgePaddingLeft: Int = 4 * dpi,
var badgePaddingRight: Int = 4 * dpi,
var badgePaddingTop: Int = 0,
var badgePaddingBottom: Int = 0,
var badgeAnchorChildIndex: Int = -1,
var badgeIgnoreChildPadding: Boolean = true,
/**最小的高度大小, px. 大于0生效; 圆点时属性无效*/
var badgeMinHeight: Int = -2,
/**最小的宽度大小, px. 大于0生效; 圆点时属性无效;
* -1 表示使用使用计算出来的高度值*/
var badgeMinWidth: Int = -1
)

View File

@ -0,0 +1,279 @@
package com.angcyo.tablayout
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.core.view.ViewCompat
/**
* 边框绘制, 支持首尾圆角中间不圆角的样式
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/27
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
open class DslTabBorder : DslGradientDrawable() {
/**
* 是否要接管[itemView]背景的绘制
* [updateItemBackground]
* */
var borderDrawItemBackground: Boolean = true
/**是否保持每个[itemView]的圆角都一样, 否则首尾有圆角, 中间没有圆角*/
var borderKeepItemRadius: Boolean = false
var borderBackgroundDrawable: Drawable? = null
/**宽度补偿*/
var borderBackgroundWidthOffset: Int = 0
/**高度补偿*/
var borderBackgroundHeightOffset: Int = 0
/**强制指定选中item的背景颜色*/
var borderItemBackgroundSolidColor: Int? = null
/**当item不可选中时的背景绘制颜色
* [com.angcyo.tablayout.DslTabLayout.itemEnableSelector]
* [borderItemBackgroundSolidColor]*/
var borderItemBackgroundSolidDisableColor: Int? = null
/**强制指定选中item的背景渐变颜色*/
var borderItemBackgroundGradientColors: IntArray? = null
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
val typedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
val borderBackgroundColor =
typedArray.getColor(R.styleable.DslTabLayout_tab_border_solid_color, gradientSolidColor)
gradientStrokeColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_border_stroke_color,
gradientStrokeColor
)
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_border_stroke_width,
2 * dpi
)
val radiusSize =
typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_border_radius_size, 0)
cornerRadius(radiusSize.toFloat())
originDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_border_drawable)
borderDrawItemBackground = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_border_draw_item_background,
borderDrawItemBackground
)
borderKeepItemRadius = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_border_keep_item_radius,
borderKeepItemRadius
)
borderBackgroundWidthOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_border_item_background_width_offset,
borderBackgroundWidthOffset
)
borderBackgroundHeightOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_border_item_background_height_offset,
borderBackgroundHeightOffset
)
//
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_solid_color)) {
borderItemBackgroundSolidColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_border_item_background_solid_color,
gradientStrokeColor
)
}
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_solid_disable_color)) {
borderItemBackgroundSolidDisableColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_border_item_background_solid_disable_color,
borderItemBackgroundSolidColor ?: gradientStrokeColor
)
}
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_gradient_start_color) ||
typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_gradient_end_color)
) {
val startColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_border_item_background_gradient_start_color,
gradientStrokeColor
)
val endColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_border_item_background_gradient_end_color,
gradientStrokeColor
)
borderItemBackgroundGradientColors = intArrayOf(startColor, endColor)
}
typedArray.recycle()
if (originDrawable == null) {
//无自定义的drawable, 那么自绘.
borderBackgroundDrawable = DslGradientDrawable().configDrawable {
gradientSolidColor = borderBackgroundColor
gradientRadii = this@DslTabBorder.gradientRadii
}.originDrawable
updateOriginDrawable()
}
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
originDrawable?.apply {
setBounds(
paddingLeft,
paddingBottom,
viewWidth - paddingRight,
viewHeight - paddingBottom
)
draw(canvas)
}
}
fun drawBorderBackground(canvas: Canvas) {
super.draw(canvas)
borderBackgroundDrawable?.apply {
setBounds(
paddingLeft,
paddingBottom,
viewWidth - paddingRight,
viewHeight - paddingBottom
)
draw(canvas)
}
}
var itemSelectBgDrawable: Drawable? = null
var itemDeselectBgDrawable: Drawable? = null
/**开启边框绘制后, [itemView]的背景也需要负责设置*/
open fun updateItemBackground(
tabLayout: DslTabLayout,
itemView: View,
index: Int,
select: Boolean
) {
if (!borderDrawItemBackground) {
return
}
if (select) {
val isFirst = index == 0
val isLast = index == tabLayout.dslSelector.visibleViewList.size - 1
val drawable = DslGradientDrawable().configDrawable {
gradientWidthOffset = borderBackgroundWidthOffset
gradientHeightOffset = borderBackgroundHeightOffset
gradientSolidColor =
borderItemBackgroundSolidColor ?: this@DslTabBorder.gradientStrokeColor
if (!tabLayout.itemEnableSelector) {
if (borderItemBackgroundSolidDisableColor != null) {
gradientSolidColor = borderItemBackgroundSolidDisableColor!!
}
}
gradientColors = borderItemBackgroundGradientColors
if ((isFirst && isLast) || borderKeepItemRadius) {
//只有一个child
gradientRadii = this@DslTabBorder.gradientRadii
} else if (isFirst) {
if (tabLayout.isHorizontal()) {
if (tabLayout.isLayoutRtl) {
gradientRadii = floatArrayOf(
0f,
0f,
this@DslTabBorder.gradientRadii[2],
this@DslTabBorder.gradientRadii[3],
this@DslTabBorder.gradientRadii[4],
this@DslTabBorder.gradientRadii[5],
0f,
0f
)
} else {
gradientRadii = floatArrayOf(
this@DslTabBorder.gradientRadii[0],
this@DslTabBorder.gradientRadii[1],
0f,
0f,
0f,
0f,
this@DslTabBorder.gradientRadii[6],
this@DslTabBorder.gradientRadii[7]
)
}
} else {
gradientRadii = floatArrayOf(
this@DslTabBorder.gradientRadii[0],
this@DslTabBorder.gradientRadii[1],
this@DslTabBorder.gradientRadii[2],
this@DslTabBorder.gradientRadii[3],
0f,
0f,
0f,
0f
)
}
} else if (isLast) {
if (tabLayout.isHorizontal()) {
if (tabLayout.isLayoutRtl) {
gradientRadii = floatArrayOf(
this@DslTabBorder.gradientRadii[0],
this@DslTabBorder.gradientRadii[1],
0f,
0f,
0f,
0f,
this@DslTabBorder.gradientRadii[6],
this@DslTabBorder.gradientRadii[7]
)
} else {
gradientRadii = floatArrayOf(
0f,
0f,
this@DslTabBorder.gradientRadii[2],
this@DslTabBorder.gradientRadii[3],
this@DslTabBorder.gradientRadii[4],
this@DslTabBorder.gradientRadii[5],
0f,
0f
)
}
} else {
gradientRadii = floatArrayOf(
0f,
0f,
0f,
0f,
this@DslTabBorder.gradientRadii[4],
this@DslTabBorder.gradientRadii[5],
this@DslTabBorder.gradientRadii[6],
this@DslTabBorder.gradientRadii[7]
)
}
}
}
itemSelectBgDrawable = drawable
ViewCompat.setBackground(itemView, itemSelectBgDrawable)
} else {
ViewCompat.setBackground(itemView, itemDeselectBgDrawable)
}
}
}

View File

@ -0,0 +1,153 @@
package com.angcyo.tablayout
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.widget.LinearLayout
/**
* 垂直分割线
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/27
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
open class DslTabDivider : DslGradientDrawable() {
var dividerWidth = 2 * dpi
var dividerHeight = 2 * dpi
var dividerMarginLeft = 0
var dividerMarginRight = 0
var dividerMarginTop = 0
var dividerMarginBottom = 0
/**
* [LinearLayout.SHOW_DIVIDER_BEGINNING]
* [LinearLayout.SHOW_DIVIDER_MIDDLE]
* [LinearLayout.SHOW_DIVIDER_END]
* */
var dividerShowMode = LinearLayout.SHOW_DIVIDER_MIDDLE
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
super.initAttribute(context, attributeSet)
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
dividerWidth = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_width,
dividerWidth
)
dividerHeight = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_height,
dividerHeight
)
dividerMarginLeft = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_margin_left,
dividerMarginLeft
)
dividerMarginRight = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_margin_right,
dividerMarginRight
)
dividerMarginTop = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_margin_top,
dividerMarginTop
)
dividerMarginBottom = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_margin_bottom,
dividerMarginBottom
)
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_divider_solid_color)) {
gradientSolidColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_divider_solid_color,
gradientSolidColor
)
} else if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_stroke_color)) {
gradientSolidColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_border_stroke_color,
gradientSolidColor
)
} else {
gradientSolidColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_deselect_color,
gradientSolidColor
)
}
gradientStrokeColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_divider_stroke_color,
gradientStrokeColor
)
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_stroke_width,
0
)
val radiusSize =
typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_divider_radius_size,
2 * dpi
)
cornerRadius(radiusSize.toFloat())
originDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_divider_drawable)
dividerShowMode =
typedArray.getInt(R.styleable.DslTabLayout_tab_divider_show_mode, dividerShowMode)
typedArray.recycle()
if (originDrawable == null) {
//无自定义的drawable, 那么自绘.
updateOriginDrawable()
}
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
originDrawable?.apply {
bounds = this@DslTabDivider.bounds
draw(canvas)
}
}
val _tabLayout: DslTabLayout?
get() = if (callback is DslTabLayout) callback as DslTabLayout else null
/**
* [childIndex]位置前面是否需要分割线
* */
open fun haveBeforeDivider(childIndex: Int, childCount: Int): Boolean {
val tabLayout = _tabLayout
if (tabLayout != null && tabLayout.isHorizontal() && tabLayout.isLayoutRtl) {
if (childIndex == 0) {
return dividerShowMode and LinearLayout.SHOW_DIVIDER_END != 0
}
return dividerShowMode and LinearLayout.SHOW_DIVIDER_MIDDLE != 0
}
if (childIndex == 0) {
return dividerShowMode and LinearLayout.SHOW_DIVIDER_BEGINNING != 0
}
return dividerShowMode and LinearLayout.SHOW_DIVIDER_MIDDLE != 0
}
/**
* [childIndex]位置后面是否需要分割线
* */
open fun haveAfterDivider(childIndex: Int, childCount: Int): Boolean {
val tabLayout = _tabLayout
if (tabLayout != null && tabLayout.isHorizontal() && tabLayout.isLayoutRtl) {
if (childIndex == childCount - 1) {
return dividerShowMode and LinearLayout.SHOW_DIVIDER_BEGINNING != 0
}
}
if (childIndex == childCount - 1) {
return dividerShowMode and LinearLayout.SHOW_DIVIDER_END != 0
}
return false
}
}

View File

@ -0,0 +1,118 @@
package com.angcyo.tablayout
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.ViewGroup
/**
*
* Email:angcyo@126.com
* @author angcyo
* @date 2021/05/19
* Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved.
*/
open class DslTabHighlight(val tabLayout: DslTabLayout) : DslGradientDrawable() {
/**需要绘制的Drawable*/
var highlightDrawable: Drawable? = null
/**宽度测量模式*/
var highlightWidth = ViewGroup.LayoutParams.MATCH_PARENT
/**高度测量模式*/
var highlightHeight = ViewGroup.LayoutParams.MATCH_PARENT
/**宽度补偿*/
var highlightWidthOffset = 0
/**高度补偿*/
var highlightHeightOffset = 0
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
//super.initAttribute(context, attributeSet)
val typedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
highlightDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_highlight_drawable)
highlightWidth = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_highlight_width,
highlightWidth
)
highlightHeight = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_highlight_height,
highlightHeight
)
highlightWidthOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_highlight_width_offset,
highlightWidthOffset
)
highlightHeightOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_highlight_height_offset,
highlightHeightOffset
)
typedArray.recycle()
if (highlightDrawable == null && isValidConfig()) {
updateOriginDrawable()
}
}
override fun updateOriginDrawable(): GradientDrawable? {
val drawable = super.updateOriginDrawable()
highlightDrawable = originDrawable
return drawable
}
override fun draw(canvas: Canvas) {
//super.draw(canvas)
val itemView = tabLayout.currentItemView
if (itemView != null) {
val lp = itemView.layoutParams
if (lp is DslTabLayout.LayoutParams) {
lp.highlightDrawable ?: highlightDrawable
} else {
highlightDrawable
}?.apply {
val drawWidth: Int = when (highlightWidth) {
ViewGroup.LayoutParams.MATCH_PARENT -> itemView.measuredWidth
ViewGroup.LayoutParams.WRAP_CONTENT -> intrinsicWidth
else -> highlightWidth
} + highlightWidthOffset
val drawHeight: Int = when (highlightHeight) {
ViewGroup.LayoutParams.MATCH_PARENT -> itemView.measuredHeight
ViewGroup.LayoutParams.WRAP_CONTENT -> intrinsicHeight
else -> highlightHeight
} + highlightHeightOffset
val centerX: Int = itemView.left + (itemView.right - itemView.left) / 2
val centerY: Int = itemView.top + (itemView.bottom - itemView.top) / 2
setBounds(
centerX - drawWidth / 2,
centerY - drawHeight / 2,
centerX + drawWidth / 2,
centerY + drawHeight / 2
)
draw(canvas)
canvas.save()
if (tabLayout.isHorizontal()) {
canvas.translate(itemView.left.toFloat(), 0f)
} else {
canvas.translate(0f, itemView.top.toFloat())
}
itemView.draw(canvas)
canvas.restore()
}
}
}
}

View File

@ -0,0 +1,931 @@
package com.angcyo.tablayout
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.withSave
import java.util.*
import kotlin.math.absoluteValue
import kotlin.math.max
/**
* 指示器
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/25
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
open class DslTabIndicator(val tabLayout: DslTabLayout) : DslGradientDrawable() {
companion object {
/**非颜色值*/
const val NO_COLOR = -2
//---style---
/**不绘制指示器*/
const val INDICATOR_STYLE_NONE = 0
/**指示器绘制在[itemView]的顶部*/
const val INDICATOR_STYLE_TOP = 0x1
/**指示器绘制在[itemView]的底部*/
const val INDICATOR_STYLE_BOTTOM = 0x2
/**默认样式,指示器绘制在[itemView]的中心*/
const val INDICATOR_STYLE_CENTER = 0x4
/**前景绘制,
* 默认是背景绘制, 指示器绘制[itemView]的背部, [itemView] 请不要设置background, 否则可能看不见*/
const val INDICATOR_STYLE_FOREGROUND = 0x1000
//---gravity---
/**指示器重力在开始的位置(横向左边, 纵向上边)*/
const val INDICATOR_GRAVITY_START = 0x1
/**指示器重力在结束的位置(横向右边, 纵向下边)*/
const val INDICATOR_GRAVITY_END = 0x2
/**指示器重力在中间*/
const val INDICATOR_GRAVITY_CENTER = 0x4
}
/**指示器绘制的样式*/
var indicatorStyle = INDICATOR_STYLE_NONE //初始化
/**[indicatorStyle]*/
val _indicatorDrawStyle: Int
get() = indicatorStyle.remove(INDICATOR_STYLE_FOREGROUND)
/**优先将指示器显示在[DslTabLayout]的什么位置
* [INDICATOR_GRAVITY_START] 开始的位置
* [INDICATOR_GRAVITY_END] 结束的位置
* [INDICATOR_GRAVITY_CENTER] 中间的位置*/
var indicatorGravity = INDICATOR_GRAVITY_CENTER
/**
* 指示器在流向下一个位置时, 是否采用[Flow]流线的方式改变宽度
* */
var indicatorEnableFlow: Boolean = false
/**指示器闪现效果, 从当前位置直接跨越到目标位置*/
var indicatorEnableFlash: Boolean = false
/**使用clip的方式绘制闪现效果*/
var indicatorEnableFlashClip: Boolean = true
/**当目标和当前的索引差值<=此值时, [Flow]效果才有效*/
var indicatorFlowStep: Int = 1
/**指示器绘制实体*/
var indicatorDrawable: Drawable? = null
set(value) {
field = tintDrawableColor(value, indicatorColor)
}
/**过滤[indicatorDrawable]的颜色*/
var indicatorColor: Int = NO_COLOR
set(value) {
field = value
indicatorDrawable = indicatorDrawable
}
/**
* 指示器的宽度
* WRAP_CONTENT: [childView]内容的宽度,
* MATCH_PARENT: [childView]的宽度
* 40dp: 固定值
* */
var indicatorWidth = 0 //初始化
/**宽度补偿*/
var indicatorWidthOffset = 0
/**
* 指示器的高度
* WRAP_CONTENT: [childView]内容的高度,
* MATCH_PARENT: [childView]的高度
* 40dp: 固定值
* */
var indicatorHeight = 0 //初始化
/**高度补偿*/
var indicatorHeightOffset = 0
/**XY轴方向补偿*/
var indicatorXOffset = 0
/**会根据[indicatorStyle]自动取负值*/
var indicatorYOffset = 0
/**
* 宽高[WRAP_CONTENT], 内容view的定位索引
* */
var indicatorContentIndex = -1
var indicatorContentId = View.NO_ID
/**切换时是否需要动画的支持*/
var indicatorAnim = true
/**在获取锚点view的宽高时, 是否需要忽略对应的padding属性*/
var ignoreChildPadding: Boolean = true
init {
callback = tabLayout
}
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
val typedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
indicatorDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_indicator_drawable)
indicatorColor =
typedArray.getColor(R.styleable.DslTabLayout_tab_indicator_color, indicatorColor)
indicatorStyle = typedArray.getInt(
R.styleable.DslTabLayout_tab_indicator_style,
if (tabLayout.isHorizontal()) INDICATOR_STYLE_BOTTOM else INDICATOR_STYLE_TOP
)
indicatorGravity = typedArray.getInt(
R.styleable.DslTabLayout_tab_indicator_gravity,
indicatorGravity
)
//初始化指示器的高度和宽度
if (indicatorStyle.have(INDICATOR_STYLE_FOREGROUND)) {
//前景绘制
indicatorWidth = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_indicator_width,
if (tabLayout.isHorizontal()) ViewGroup.LayoutParams.MATCH_PARENT else 3 * dpi
)
indicatorHeight = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_indicator_height,
if (tabLayout.isHorizontal()) 3 * dpi else ViewGroup.LayoutParams.MATCH_PARENT
)
indicatorXOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_x_offset,
if (tabLayout.isHorizontal()) 0 else 2 * dpi
)
indicatorYOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_y_offset,
if (tabLayout.isHorizontal()) 2 * dpi else 0
)
} else {
//背景绘制样式
if (tabLayout.isHorizontal()) {
indicatorWidth = ViewGroup.LayoutParams.MATCH_PARENT
indicatorHeight = ViewGroup.LayoutParams.MATCH_PARENT
} else {
indicatorHeight = ViewGroup.LayoutParams.MATCH_PARENT
indicatorWidth = ViewGroup.LayoutParams.MATCH_PARENT
}
indicatorWidth = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_indicator_width,
indicatorWidth
)
indicatorHeight = typedArray.getLayoutDimension(
R.styleable.DslTabLayout_tab_indicator_height,
indicatorHeight
)
indicatorXOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_x_offset,
indicatorXOffset
)
indicatorYOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_y_offset,
indicatorYOffset
)
}
ignoreChildPadding = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_indicator_ignore_child_padding,
!indicatorStyle.have(INDICATOR_STYLE_CENTER)
)
indicatorFlowStep =
typedArray.getInt(R.styleable.DslTabLayout_tab_indicator_flow_step, indicatorFlowStep)
indicatorEnableFlow = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_indicator_enable_flow,
indicatorEnableFlow
)
indicatorEnableFlash = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_indicator_enable_flash,
indicatorEnableFlash
)
indicatorEnableFlashClip = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_indicator_enable_flash_clip,
indicatorEnableFlashClip
)
indicatorWidthOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_width_offset,
indicatorWidthOffset
)
indicatorHeightOffset = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_height_offset,
indicatorHeightOffset
)
indicatorContentIndex = typedArray.getInt(
R.styleable.DslTabLayout_tab_indicator_content_index,
indicatorContentIndex
)
indicatorContentId = typedArray.getResourceId(
R.styleable.DslTabLayout_tab_indicator_content_id,
indicatorContentId
)
indicatorAnim = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_indicator_anim,
indicatorAnim
)
//代码构建Drawable
gradientShape =
typedArray.getInt(R.styleable.DslTabLayout_tab_indicator_shape, gradientShape)
gradientSolidColor =
typedArray.getColor(
R.styleable.DslTabLayout_tab_indicator_solid_color,
gradientSolidColor
)
gradientStrokeColor =
typedArray.getColor(
R.styleable.DslTabLayout_tab_indicator_stroke_color,
gradientStrokeColor
)
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_stroke_width,
gradientStrokeWidth
)
gradientDashWidth = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_dash_width,
gradientDashWidth.toInt()
).toFloat()
gradientDashGap = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_indicator_dash_gap,
gradientDashGap.toInt()
).toFloat()
val gradientRadius =
typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_indicator_radius, 0)
if (gradientRadius > 0) {
Arrays.fill(gradientRadii, gradientRadius.toFloat())
} else {
typedArray.getString(R.styleable.DslTabLayout_tab_indicator_radii)?.let {
_fillRadii(gradientRadii, it)
}
}
val gradientColors =
typedArray.getString(R.styleable.DslTabLayout_tab_indicator_gradient_colors)
this.gradientColors = if (gradientColors.isNullOrEmpty()) {
val startColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_indicator_gradient_start_color,
Color.TRANSPARENT
)
val endColor = typedArray.getColor(
R.styleable.DslTabLayout_tab_indicator_gradient_end_color,
Color.TRANSPARENT
)
if (startColor != endColor) {
intArrayOf(startColor, endColor)
} else {
this.gradientColors
}
} else {
_fillColor(gradientColors) ?: this.gradientColors
}
//...end
typedArray.recycle()
if (indicatorDrawable == null && isValidConfig()) {
updateOriginDrawable()
}
}
override fun updateOriginDrawable(): GradientDrawable? {
val drawable = super.updateOriginDrawable()
indicatorDrawable = originDrawable
return drawable
}
open fun tintDrawableColor(drawable: Drawable?, color: Int): Drawable? {
if (drawable == null || color == NO_COLOR) {
return drawable
}
return drawable.tintDrawableColor(color)
}
/**指示器需要参考的目标控件*/
open fun indicatorContentView(childView: View): View? {
val lp = childView.layoutParams as DslTabLayout.LayoutParams
val contentId =
if (lp.indicatorContentId != View.NO_ID) lp.indicatorContentId else indicatorContentId
if (contentId != View.NO_ID) {
return childView.findViewById(contentId)
}
//如果child强制指定了index, 就用指定的.
val contentIndex =
if (lp.indicatorContentIndex >= 0) lp.indicatorContentIndex else indicatorContentIndex
return if (contentIndex >= 0 && childView is ViewGroup && contentIndex in 0 until childView.childCount) {
//有指定
val contentChildView = childView.getChildAt(contentIndex)
contentChildView
} else {
//没有指定
null
}
}
/**根据指定[index]索引, 获取目标[View]*/
open fun targetChildView(
index: Int,
onChildView: (childView: View, contentChildView: View?) -> Unit
) {
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
onChildView(childView, indicatorContentView(childView))
}
}
open fun getChildTargetPaddingLeft(childView: View): Int =
if (ignoreChildPadding) childView.paddingLeft else 0
open fun getChildTargetPaddingRight(childView: View): Int =
if (ignoreChildPadding) childView.paddingRight else 0
open fun getChildTargetPaddingTop(childView: View): Int =
if (ignoreChildPadding) childView.paddingTop else 0
open fun getChildTargetPaddingBottom(childView: View): Int =
if (ignoreChildPadding) childView.paddingBottom else 0
open fun getChildTargetWidth(childView: View): Int =
if (ignoreChildPadding) childView.viewDrawWidth else childView.measuredWidth
open fun getChildTargetHeight(childView: View): Int =
if (ignoreChildPadding) childView.viewDrawHeight else childView.measuredHeight
/**
* [childview]对应的中心x坐标
* */
open fun getChildTargetX(index: Int, gravity: Int = indicatorGravity): Int {
var result = if (index > 0) tabLayout.maxWidth else 0
targetChildView(index) { childView, contentChildView ->
result = if (contentChildView == null) {
when (gravity) {
INDICATOR_GRAVITY_START -> childView.left
INDICATOR_GRAVITY_END -> childView.right
else -> childView.left + getChildTargetPaddingLeft(childView) + getChildTargetWidth(
childView
) / 2
}
} else {
when (gravity) {
INDICATOR_GRAVITY_START -> childView.left + contentChildView.left
INDICATOR_GRAVITY_END -> childView.left + contentChildView.right
else -> childView.left + contentChildView.left + getChildTargetPaddingLeft(
contentChildView
) + getChildTargetWidth(
contentChildView
) / 2
}
}
}
return result
}
open fun getChildTargetY(index: Int, gravity: Int = indicatorGravity): Int {
var result = if (index > 0) tabLayout.maxHeight else 0
targetChildView(index) { childView, contentChildView ->
result = if (contentChildView == null) {
when (gravity) {
INDICATOR_GRAVITY_START -> childView.top
INDICATOR_GRAVITY_END -> childView.bottom
else -> childView.top + getChildTargetPaddingTop(childView) + getChildTargetHeight(
childView
) / 2
}
} else {
when (gravity) {
INDICATOR_GRAVITY_START -> childView.top + contentChildView.top
INDICATOR_GRAVITY_END -> childView.top + childView.bottom
else -> childView.top + contentChildView.top + getChildTargetPaddingTop(
contentChildView
) + getChildTargetHeight(
contentChildView
) / 2
}
}
}
return result
}
open fun getIndicatorDrawWidth(index: Int): Int {
var result = indicatorWidth
when (indicatorWidth) {
ViewGroup.LayoutParams.WRAP_CONTENT -> {
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
result = getChildTargetWidth(indicatorContentView(childView) ?: childView)
}
}
ViewGroup.LayoutParams.MATCH_PARENT -> {
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
result = childView.measuredWidth
}
}
}
return result + indicatorWidthOffset
}
open fun getIndicatorDrawHeight(index: Int): Int {
var result = indicatorHeight
when (indicatorHeight) {
ViewGroup.LayoutParams.WRAP_CONTENT -> {
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
result = getChildTargetHeight(indicatorContentView(childView) ?: childView)
}
}
ViewGroup.LayoutParams.MATCH_PARENT -> {
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
result = childView.measuredHeight
}
}
}
return result + indicatorHeightOffset
}
override fun draw(canvas: Canvas) {
//super.draw(canvas)
if (!isVisible || _indicatorDrawStyle == INDICATOR_STYLE_NONE || indicatorDrawable == null) {
//不绘制
return
}
if (tabLayout.isHorizontal()) {
drawHorizontal(canvas)
} else {
drawVertical(canvas)
}
}
fun drawHorizontal(canvas: Canvas) {
val childSize = tabLayout.dslSelector.visibleViewList.size
var currentIndex = currentIndex
if (_targetIndex in 0 until childSize) {
currentIndex = max(0, currentIndex)
}
if (currentIndex in 0 until childSize) {
} else {
//无效的index
return
}
//"绘制$currentIndex:$currentSelectIndex $positionOffset".logi()
val drawTargetX = getChildTargetX(currentIndex)
val drawWidth = getIndicatorDrawWidth(currentIndex)
val drawHeight = getIndicatorDrawHeight(currentIndex)
val drawLeft = drawTargetX - drawWidth / 2 + indicatorXOffset
//动画过程中的left
var animLeft = drawLeft
//width
var animWidth = drawWidth
//动画执行过程中, 高度额外变大的值
var animExHeight = 0
//end value
val nextDrawTargetX = getChildTargetX(_targetIndex)
val nextDrawWidth = getIndicatorDrawWidth(_targetIndex)
val nextDrawLeft = nextDrawTargetX - nextDrawWidth / 2 + indicatorXOffset
var animEndWidth = nextDrawWidth
var animEndLeft = nextDrawLeft
if (_targetIndex in 0 until childSize && _targetIndex != currentIndex) {
//动画过程参数计算变量
val animStartLeft = drawLeft
val animStartWidth = drawWidth
val animEndHeight = getIndicatorDrawHeight(_targetIndex)
if (indicatorEnableFlash) {
//闪现效果
animWidth = (animWidth * (1 - positionOffset)).toInt()
animEndWidth = (animEndWidth * positionOffset).toInt()
animLeft = drawTargetX - animWidth / 2 + indicatorXOffset
animEndLeft = nextDrawLeft
} else if (indicatorEnableFlow && (_targetIndex - currentIndex).absoluteValue <= indicatorFlowStep) {
//激活了流动效果
val flowEndWidth: Int
if (_targetIndex > currentIndex) {
flowEndWidth = animEndLeft - animStartLeft + animEndWidth
//目标在右边
animLeft = if (positionOffset >= 0.5) {
(animStartLeft + (animEndLeft - animStartLeft) * (positionOffset - 0.5) / 0.5f).toInt()
} else {
animStartLeft
}
} else {
flowEndWidth = animStartLeft - animEndLeft + animStartWidth
//目标在左边
animLeft = if (positionOffset >= 0.5) {
animEndLeft
} else {
(animStartLeft - (animStartLeft - animEndLeft) * positionOffset / 0.5f).toInt()
}
}
animWidth = if (positionOffset >= 0.5) {
(flowEndWidth - (flowEndWidth - animEndWidth) * (positionOffset - 0.5) / 0.5f).toInt()
} else {
(animStartWidth + (flowEndWidth - animStartWidth) * positionOffset / 0.5f).toInt()
}
} else {
//默认平移效果
if (_targetIndex > currentIndex) {
//目标在右边
animLeft =
(animStartLeft + (animEndLeft - animStartLeft) * positionOffset).toInt()
} else {
//目标在左边
animLeft =
(animStartLeft - (animStartLeft - animEndLeft) * positionOffset).toInt()
}
//动画过程中的宽度
animWidth =
(animStartWidth + (animEndWidth - animStartWidth) * positionOffset).toInt()
}
animExHeight = ((animEndHeight - drawHeight) * positionOffset).toInt()
}
//前景
val drawTop = when (_indicatorDrawStyle) {
//底部绘制
INDICATOR_STYLE_BOTTOM -> viewHeight - drawHeight - indicatorYOffset
//顶部绘制
INDICATOR_STYLE_TOP -> 0 + indicatorYOffset
//居中绘制
else -> paddingTop + viewDrawHeight / 2 - drawHeight / 2 + indicatorYOffset -
animExHeight +
(tabLayout._maxConvexHeight - _childConvexHeight(currentIndex)) / 2
}
indicatorDrawable?.apply {
if (indicatorEnableFlash) {
//flash
if (indicatorEnableFlashClip) {
drawIndicatorClipHorizontal(
this,
canvas,
drawLeft,
drawTop,
drawLeft + drawWidth,
drawTop + drawHeight + animExHeight,
animWidth,
1 - positionOffset
)
} else {
drawIndicator(
this, canvas, animLeft,
drawTop,
animLeft + animWidth,
drawTop + drawHeight + animExHeight,
1 - positionOffset
)
}
if (_targetIndex in 0 until childSize) {
if (indicatorEnableFlashClip) {
drawIndicatorClipHorizontal(
this,
canvas,
nextDrawLeft,
drawTop,
nextDrawLeft + nextDrawWidth,
drawTop + drawHeight + animExHeight,
animEndWidth,
positionOffset
)
} else {
drawIndicator(
this, canvas, animEndLeft,
drawTop,
animEndLeft + animEndWidth,
drawTop + drawHeight + animExHeight,
positionOffset
)
}
}
} else {
//normal
drawIndicator(
this, canvas, animLeft,
drawTop,
animLeft + animWidth,
drawTop + drawHeight + animExHeight,
1 - positionOffset
)
}
}
}
fun drawIndicator(
indicator: Drawable,
canvas: Canvas,
l: Int,
t: Int,
r: Int,
b: Int,
offset: Float
) {
indicator.apply {
if (this is ITabIndicatorDraw) {
setBounds(l, t, r, b)
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
} else {
val width = r - l
val height = b - t
setBounds(0, 0, width, height)
canvas.withSave {
translate(l.toFloat(), t.toFloat())
draw(canvas)
}
}
}
}
fun drawIndicatorClipHorizontal(
indicator: Drawable,
canvas: Canvas,
l: Int,
t: Int,
r: Int,
b: Int,
endWidth: Int,
offset: Float
) {
indicator.apply {
canvas.save()
val dx = (r - l - endWidth) / 2
canvas.clipRect(l + dx, t, r - dx, b)
setBounds(l, t, r, b)
if (this is ITabIndicatorDraw) {
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
} else {
draw(canvas)
}
canvas.restore()
}
}
fun drawIndicatorClipVertical(
indicator: Drawable,
canvas: Canvas,
l: Int,
t: Int,
r: Int,
b: Int,
endHeight: Int,
offset: Float
) {
indicator.apply {
canvas.save()
val dy = (b - t - endHeight) / 2
canvas.clipRect(l, t + dy, r, b - dy)
setBounds(l, t, r, b)
if (this is ITabIndicatorDraw) {
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
} else {
draw(canvas)
}
canvas.restore()
}
}
fun drawVertical(canvas: Canvas) {
val childSize = tabLayout.dslSelector.visibleViewList.size
var currentIndex = currentIndex
if (_targetIndex in 0 until childSize) {
currentIndex = max(0, currentIndex)
}
if (currentIndex in 0 until childSize) {
} else {
//无效的index
return
}
//"绘制$currentIndex:$currentSelectIndex $positionOffset".logi()
val drawTargetY = getChildTargetY(currentIndex)
val drawWidth = getIndicatorDrawWidth(currentIndex)
val drawHeight = getIndicatorDrawHeight(currentIndex)
val drawTop = drawTargetY - drawHeight / 2 + indicatorYOffset
//动画过程中的top
var animTop = drawTop
//height
var animHeight = drawHeight
//动画执行过程中, 宽度额外变大的值
var animExWidth = 0
//end value
val nextDrawTargetY = getChildTargetY(_targetIndex)
val nextDrawHeight = getIndicatorDrawHeight(_targetIndex)
val nextDrawTop = nextDrawTargetY - nextDrawHeight / 2 + indicatorYOffset
var animEndHeight = nextDrawHeight
var animEndTop = nextDrawTop
if (_targetIndex in 0 until childSize && _targetIndex != currentIndex) {
//动画过程参数计算变量
val animStartTop = drawTop
val animStartHeight = drawHeight
val animEndWidth = getIndicatorDrawWidth(_targetIndex)
if (indicatorEnableFlash) {
//闪现效果
animHeight = (animHeight * (1 - positionOffset)).toInt()
animEndHeight = (animEndHeight * positionOffset).toInt()
animTop = drawTargetY - animHeight / 2 + indicatorXOffset
animEndTop = nextDrawTargetY - animEndHeight / 2 + indicatorXOffset
} else if (indicatorEnableFlow && (_targetIndex - currentIndex).absoluteValue <= indicatorFlowStep) {
//激活了流动效果
val flowEndHeight: Int
if (_targetIndex > currentIndex) {
flowEndHeight = animEndTop - animStartTop + animEndHeight
//目标在下边
animTop = if (positionOffset >= 0.5) {
(animStartTop + (animEndTop - animStartTop) * (positionOffset - 0.5) / 0.5f).toInt()
} else {
animStartTop
}
} else {
flowEndHeight = animStartTop - animEndTop + animStartHeight
//目标在上边
animTop = if (positionOffset >= 0.5) {
animEndTop
} else {
(animStartTop - (animStartTop - animEndTop) * positionOffset / 0.5f).toInt()
}
}
animHeight = if (positionOffset >= 0.5) {
(flowEndHeight - (flowEndHeight - animEndHeight) * (positionOffset - 0.5) / 0.5f).toInt()
} else {
(animStartHeight + (flowEndHeight - animStartHeight) * positionOffset / 0.5f).toInt()
}
} else {
if (_targetIndex > currentIndex) {
//目标在下边
animTop = (animStartTop + (animEndTop - animStartTop) * positionOffset).toInt()
} else {
//目标在上边
animTop = (animStartTop - (animStartTop - animEndTop) * positionOffset).toInt()
}
//动画过程中的宽度
animHeight =
(animStartHeight + (animEndHeight - animStartHeight) * positionOffset).toInt()
}
animExWidth = ((animEndWidth - drawWidth) * positionOffset).toInt()
}
val drawLeft = when (_indicatorDrawStyle) {
INDICATOR_STYLE_BOTTOM -> {
//右边/底部绘制
viewWidth - drawWidth - indicatorXOffset
}
INDICATOR_STYLE_TOP -> {
//左边/顶部绘制
0 + indicatorXOffset
}
else -> {
//居中绘制
paddingLeft + indicatorXOffset + (viewDrawWidth / 2 - drawWidth / 2) -
(tabLayout._maxConvexHeight - _childConvexHeight(currentIndex)) / 2
}
}
indicatorDrawable?.apply {
//flash
if (indicatorEnableFlash) {
if (indicatorEnableFlashClip) {
drawIndicatorClipVertical(
this, canvas, drawLeft,
drawTop,
drawLeft + drawWidth + animExWidth,
drawTop + drawHeight,
animHeight,
1 - positionOffset
)
} else {
drawIndicator(
this, canvas, drawLeft,
animTop,
drawLeft + drawWidth + animExWidth,
animTop + animHeight,
1 - positionOffset
)
}
if (_targetIndex in 0 until childSize) {
if (indicatorEnableFlashClip) {
drawIndicatorClipVertical(
this, canvas, drawLeft,
nextDrawTop,
drawLeft + drawWidth + animExWidth,
nextDrawTop + nextDrawHeight,
animEndHeight,
positionOffset
)
} else {
drawIndicator(
this, canvas, drawLeft,
animEndTop,
drawLeft + drawWidth + animExWidth,
animEndTop + animEndHeight,
positionOffset
)
}
}
} else {
drawIndicator(
this, canvas, drawLeft,
animTop,
drawLeft + drawWidth + animExWidth,
animTop + animHeight,
1 - positionOffset
)
}
}
}
fun _childConvexHeight(index: Int): Int {
if (attachView is ViewGroup) {
((attachView as ViewGroup).getChildAt(index).layoutParams as? DslTabLayout.LayoutParams)?.apply {
return layoutConvexHeight
}
}
return 0
}
/**
* 距离[_targetIndex]的偏移比例.[0->1]的过程
* */
var positionOffset: Float = 0f
set(value) {
field = value
invalidateSelf()
}
/**当前绘制的index*/
var currentIndex: Int = -1
/**滚动目标的index*/
var _targetIndex = -1
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,526 @@
package com.angcyo.tablayout
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.IdRes
import com.angcyo.tablayout.DslTabIndicator.Companion.NO_COLOR
import kotlin.math.max
import kotlin.math.min
/**
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/26
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
*/
open class DslTabLayoutConfig(val tabLayout: DslTabLayout) : DslSelectorConfig() {
/**是否开启文本颜色*/
var tabEnableTextColor = true
set(value) {
field = value
if (field) {
tabEnableIcoColor = true
}
}
/**是否开启颜色渐变效果*/
var tabEnableGradientColor = false
set(value) {
field = value
if (field) {
tabEnableIcoGradientColor = true
}
}
/**是否激活指示器的颜色渐变效果*/
var tabEnableIndicatorGradientColor = false
/**选中的文本颜色*/
var tabSelectColor: Int = Color.WHITE //Color.parseColor("#333333")
/**未选中的文本颜色*/
var tabDeselectColor: Int = Color.parseColor("#999999")
/**是否开启Bold, 文本加粗*/
var tabEnableTextBold = false
/**是否使用粗体字体的方式设置粗体, 否则使用[Paint.FAKE_BOLD_TEXT_FLAG]
* 需要先激活[tabEnableTextBold]*/
var tabUseTypefaceBold = false
/**是否开启图标颜色*/
var tabEnableIcoColor = true
/**是否开启图标颜色渐变效果*/
var tabEnableIcoGradientColor = false
/**选中的图标颜色*/
var tabIcoSelectColor: Int = NO_COLOR
get() {
return if (field == NO_COLOR) tabSelectColor else field
}
/**未选中的图标颜色*/
var tabIcoDeselectColor: Int = NO_COLOR
get() {
return if (field == NO_COLOR) tabDeselectColor else field
}
/**是否开启scale渐变效果*/
var tabEnableGradientScale = false
/**最小缩放的比例*/
var tabMinScale = 0.8f
/**最大缩放的比例*/
var tabMaxScale = 1.2f
/**是否开启字体大小渐变效果*/
var tabEnableGradientTextSize = true
/**tab中文本字体未选中时的字体大小, >0时激活*/
var tabTextMinSize = -1f
/**tab中文本字体选中时的字体大小, >0时激活*/
var tabTextMaxSize = -1f
/**渐变效果实现的回调*/
var tabGradientCallback = TabGradientCallback()
/**指定文本控件的id, 所有文本属性改变, 将会发生在这个控件上.
* 如果指定的控件不存在, 控件会降权至[ItemView]*/
@IdRes
var tabTextViewId: Int = View.NO_ID
/**指定图标控件的id*/
@IdRes
var tabIconViewId: Int = View.NO_ID
/**返回用于配置文本样式的控件*/
var onGetTextStyleView: (itemView: View, index: Int) -> TextView? = { itemView, _ ->
if (tabTextViewId == View.NO_ID) {
var tv: TextView? = if (itemView is TextView) itemView else null
if (tabLayout.tabIndicator.indicatorContentIndex != -1) {
itemView.getChildOrNull(tabLayout.tabIndicator.indicatorContentIndex)?.let {
if (it is TextView) {
tv = it
}
}
}
if (tabLayout.tabIndicator.indicatorContentId != View.NO_ID) {
itemView.findViewById<View>(tabLayout.tabIndicator.indicatorContentId)?.let {
if (it is TextView) {
tv = it
}
}
}
val lp = itemView.layoutParams
if (lp is DslTabLayout.LayoutParams) {
if (lp.indicatorContentIndex != -1 && itemView is ViewGroup) {
itemView.getChildOrNull(lp.indicatorContentIndex)?.let {
if (it is TextView) {
tv = it
}
}
}
if (lp.indicatorContentId != View.NO_ID) {
itemView.findViewById<View>(lp.indicatorContentId)?.let {
if (it is TextView) {
tv = it
}
}
}
if (lp.contentTextViewIndex != -1 && itemView is ViewGroup) {
itemView.getChildOrNull(lp.contentTextViewIndex)?.let {
if (it is TextView) {
tv = it
}
}
}
if (lp.contentTextViewId != View.NO_ID) {
itemView.findViewById<View>(lp.contentTextViewId)?.let {
if (it is TextView) {
tv = it
}
}
}
}
tv
} else {
itemView.findViewById(tabTextViewId)
}
}
/**返回用于配置ico样式的控件*/
var onGetIcoStyleView: (itemView: View, index: Int) -> View? = { itemView, _ ->
if (tabIconViewId == View.NO_ID) {
var iv: View? = itemView
if (tabLayout.tabIndicator.indicatorContentIndex != -1) {
itemView.getChildOrNull(tabLayout.tabIndicator.indicatorContentIndex)?.let {
iv = it
}
}
if (tabLayout.tabIndicator.indicatorContentId != View.NO_ID) {
itemView.findViewById<View>(tabLayout.tabIndicator.indicatorContentId)?.let {
iv = it
}
}
val lp = itemView.layoutParams
if (lp is DslTabLayout.LayoutParams) {
if (lp.indicatorContentIndex != -1 && itemView is ViewGroup) {
iv = itemView.getChildOrNull(lp.indicatorContentIndex)
}
if (lp.indicatorContentId != View.NO_ID) {
itemView.findViewById<View>(lp.indicatorContentId)?.let {
iv = it
}
}
if (lp.contentIconViewIndex != -1 && itemView is ViewGroup) {
iv = itemView.getChildOrNull(lp.contentIconViewIndex)
}
if (lp.contentIconViewId != View.NO_ID) {
itemView.findViewById<View>(lp.contentIconViewId)?.let {
iv = it
}
}
}
iv
} else {
itemView.findViewById(tabIconViewId)
}
}
/**获取渐变结束时,指示器的颜色.*/
var onGetGradientIndicatorColor: (fromIndex: Int, toIndex: Int, positionOffset: Float) -> Int =
{ fromIndex, toIndex, positionOffset ->
tabLayout.tabIndicator.indicatorColor
}
init {
onStyleItemView = { itemView, index, select ->
onUpdateItemStyle(itemView, index, select)
}
onSelectIndexChange = { fromIndex, selectIndexList, reselect, fromUser ->
val toIndex = selectIndexList.last()
tabLayout._viewPagerDelegate?.onSetCurrentItem(fromIndex, toIndex, reselect, fromUser)
}
}
/**xml属性读取*/
open fun initAttribute(context: Context, attributeSet: AttributeSet? = null) {
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
tabSelectColor =
typedArray.getColor(R.styleable.DslTabLayout_tab_select_color, tabSelectColor)
tabDeselectColor =
typedArray.getColor(
R.styleable.DslTabLayout_tab_deselect_color,
tabDeselectColor
)
tabIcoSelectColor =
typedArray.getColor(R.styleable.DslTabLayout_tab_ico_select_color, NO_COLOR)
tabIcoDeselectColor =
typedArray.getColor(R.styleable.DslTabLayout_tab_ico_deselect_color, NO_COLOR)
tabEnableTextColor = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_text_color,
tabEnableTextColor
)
tabEnableIndicatorGradientColor = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_indicator_gradient_color,
tabEnableIndicatorGradientColor
)
tabEnableGradientColor = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_gradient_color,
tabEnableGradientColor
)
tabEnableIcoColor = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_ico_color,
tabEnableIcoColor
)
tabEnableIcoGradientColor = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_ico_gradient_color,
tabEnableIcoGradientColor
)
tabEnableTextBold = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_text_bold,
tabEnableTextBold
)
tabUseTypefaceBold = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_use_typeface_bold,
tabUseTypefaceBold
)
tabEnableGradientScale = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_gradient_scale,
tabEnableGradientScale
)
tabMinScale = typedArray.getFloat(R.styleable.DslTabLayout_tab_min_scale, tabMinScale)
tabMaxScale = typedArray.getFloat(R.styleable.DslTabLayout_tab_max_scale, tabMaxScale)
tabEnableGradientTextSize = typedArray.getBoolean(
R.styleable.DslTabLayout_tab_enable_gradient_text_size,
tabEnableGradientTextSize
)
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_text_min_size)) {
tabTextMinSize = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_text_min_size,
tabTextMinSize.toInt()
).toFloat()
}
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_text_max_size)) {
tabTextMaxSize = typedArray.getDimensionPixelOffset(
R.styleable.DslTabLayout_tab_text_max_size,
tabTextMaxSize.toInt()
).toFloat()
}
tabTextViewId =
typedArray.getResourceId(R.styleable.DslTabLayout_tab_text_view_id, tabTextViewId)
tabIconViewId =
typedArray.getResourceId(R.styleable.DslTabLayout_tab_icon_view_id, tabIconViewId)
typedArray.recycle()
}
/**更新item的样式*/
open fun onUpdateItemStyle(itemView: View, index: Int, select: Boolean) {
//"$itemView\n$index\n$select".logw()
(onGetTextStyleView(itemView, index))?.apply {
//文本加粗
paint?.apply {
if (tabEnableTextBold && select) {
//设置粗体
if (tabUseTypefaceBold) {
typeface = Typeface.defaultFromStyle(Typeface.BOLD)
} else {
flags = flags or Paint.FAKE_BOLD_TEXT_FLAG
isFakeBoldText = true
}
} else {
//取消粗体
if (tabUseTypefaceBold) {
typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
} else {
flags = flags and Paint.FAKE_BOLD_TEXT_FLAG.inv()
isFakeBoldText = false
}
}
}
if (tabEnableTextColor) {
//文本颜色
setTextColor(if (select) tabSelectColor else tabDeselectColor)
}
if (tabTextMaxSize > 0 || tabTextMinSize > 0) {
//文本字体大小
val minTextSize = min(tabTextMinSize, tabTextMaxSize)
val maxTextSize = max(tabTextMinSize, tabTextMaxSize)
setTextSize(
TypedValue.COMPLEX_UNIT_PX,
if (select) maxTextSize else minTextSize
)
}
}
if (tabEnableIcoColor) {
onGetIcoStyleView(itemView, index)?.apply {
_updateIcoColor(this, if (select) tabIcoSelectColor else tabIcoDeselectColor)
}
}
if (tabEnableGradientScale) {
itemView.scaleX = if (select) tabMaxScale else tabMinScale
itemView.scaleY = if (select) tabMaxScale else tabMinScale
}
if (tabLayout.drawBorder) {
tabLayout.tabBorder?.updateItemBackground(tabLayout, itemView, index, select)
}
}
/**
* [DslTabLayout]滚动时回调.
* */
open fun onPageIndexScrolled(fromIndex: Int, toIndex: Int, positionOffset: Float) {
}
/**
* [onPageIndexScrolled]
* */
open fun onPageViewScrolled(fromView: View?, toView: View, positionOffset: Float) {
//"$fromView\n$toView\n$positionOffset".logi()
if (fromView != toView) {
val fromIndex = tabLayout.tabIndicator.currentIndex
val toIndex = tabLayout.tabIndicator._targetIndex
if (tabEnableIndicatorGradientColor) {
val startColor = onGetGradientIndicatorColor(fromIndex, fromIndex, 0f)
val endColor = onGetGradientIndicatorColor(fromIndex, toIndex, positionOffset)
tabLayout.tabIndicator.indicatorColor =
evaluateColor(positionOffset, startColor, endColor)
}
if (tabEnableGradientColor) {
//文本渐变
fromView?.apply {
_gradientColor(
onGetTextStyleView(this, fromIndex),
tabSelectColor,
tabDeselectColor,
positionOffset
)
}
_gradientColor(
onGetTextStyleView(toView, toIndex),
tabDeselectColor,
tabSelectColor,
positionOffset
)
}
if (tabEnableIcoGradientColor) {
//图标渐变
fromView?.apply {
_gradientIcoColor(
onGetIcoStyleView(this, fromIndex),
tabIcoSelectColor,
tabIcoDeselectColor,
positionOffset
)
}
_gradientIcoColor(
onGetIcoStyleView(toView, toIndex),
tabIcoDeselectColor,
tabIcoSelectColor,
positionOffset
)
}
if (tabEnableGradientScale) {
//scale渐变
_gradientScale(fromView, tabMaxScale, tabMinScale, positionOffset)
_gradientScale(toView, tabMinScale, tabMaxScale, positionOffset)
}
if (tabEnableGradientTextSize &&
tabTextMaxSize > 0 &&
tabTextMinSize > 0 &&
tabTextMinSize != tabTextMaxSize
) {
//文本字体大小渐变
_gradientTextSize(
fromView?.run { onGetTextStyleView(this, fromIndex) },
tabTextMaxSize,
tabTextMinSize,
positionOffset
)
_gradientTextSize(
onGetTextStyleView(toView, toIndex),
tabTextMinSize,
tabTextMaxSize,
positionOffset
)
if (toIndex == tabLayout.dslSelector.visibleViewList.lastIndex || toIndex == 0) {
tabLayout._scrollToTarget(toIndex, false)
}
}
}
}
open fun _gradientColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
tabGradientCallback.onGradientColor(view, startColor, endColor, percent)
}
open fun _gradientIcoColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
tabGradientCallback.onGradientIcoColor(view, startColor, endColor, percent)
}
open fun _gradientScale(view: View?, startScale: Float, endScale: Float, percent: Float) {
tabGradientCallback.onGradientScale(view, startScale, endScale, percent)
}
open fun _gradientTextSize(
view: TextView?,
startTextSize: Float,
endTextSize: Float,
percent: Float
) {
tabGradientCallback.onGradientTextSize(view, startTextSize, endTextSize, percent)
}
open fun _updateIcoColor(view: View?, color: Int) {
tabGradientCallback.onUpdateIcoColor(view, color)
}
}
open class TabGradientCallback {
open fun onGradientColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
(view as? TextView)?.apply {
setTextColor(evaluateColor(percent, startColor, endColor))
}
}
open fun onGradientIcoColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
onUpdateIcoColor(view, evaluateColor(percent, startColor, endColor))
}
open fun onUpdateIcoColor(view: View?, color: Int) {
view?.tintDrawableColor(color)
}
open fun onGradientScale(view: View?, startScale: Float, endScale: Float, percent: Float) {
view?.apply {
(startScale + (endScale - startScale) * percent).let {
scaleX = it
scaleY = it
}
}
}
open fun onGradientTextSize(
view: TextView?,
startTextSize: Float,
endTextSize: Float,
percent: Float
) {
view?.apply {
setTextSize(
TypedValue.COMPLEX_UNIT_PX,
(startTextSize + (endTextSize - startTextSize) * percent)
)
}
}
}

View File

@ -0,0 +1,22 @@
package com.angcyo.tablayout
import android.graphics.Canvas
/**
* 用来实现[DslTabIndicator]的自绘
* Email:angcyo@126.com
* @author angcyo
* @date 2022/02/21
* Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved.
*/
interface ITabIndicatorDraw {
/**绘制指示器
* [positionOffset] 页面偏移量*/
fun onDrawTabIndicator(
tabIndicator: DslTabIndicator,
canvas: Canvas,
positionOffset: Float
)
}

View File

@ -0,0 +1,334 @@
package com.angcyo.tablayout
import android.app.Activity
import android.content.Context
import android.content.res.Resources
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.math.MathUtils
/**
*
* Email:angcyo@126.com
* @author angcyo
* @date 2019/11/23
*/
internal val dpi: Int
get() = dp.toInt()
internal val dp: Float
get() = Resources.getSystem().displayMetrics.density
internal val View.dpi: Int
get() = context.resources.displayMetrics.density.toInt()
internal val View.screenWidth: Int
get() = context.resources.displayMetrics.widthPixels
internal val View.screenHeight: Int
get() = context.resources.displayMetrics.heightPixels
internal val View.viewDrawWidth: Int
get() = measuredWidth - paddingLeft - paddingRight
internal val View.viewDrawHeight: Int
get() = measuredHeight - paddingTop - paddingBottom
/**Match_Parent*/
internal fun exactlyMeasure(size: Int): Int =
View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
internal fun exactlyMeasure(size: Float): Int = exactlyMeasure(size.toInt())
/**Wrap_Content*/
internal fun atmostMeasure(size: Int): Int =
View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.AT_MOST)
internal fun Int.have(value: Int): Boolean = if (this == 0 || value == 0) {
false
} else if (this == 0 && value == 0) {
true
} else {
((this > 0 && value > 0) || (this < 0 && value < 0)) && this and value == value
}
internal fun Int.remove(value: Int): Int = this and value.inv()
internal fun clamp(value: Float, min: Float, max: Float): Float {
if (value < min) {
return min
} else if (value > max) {
return max
}
return value
}
internal fun clamp(value: Int, min: Int, max: Int): Int {
if (value < min) {
return min
} else if (value > max) {
return max
}
return value
}
internal fun Any.logi() {
Log.i("DslTabLayout", "$this")
}
internal fun Any.logw() {
Log.w("DslTabLayout", "$this")
}
internal fun Any.loge() {
Log.e("DslTabLayout", "$this")
}
internal fun View.calcLayoutWidthHeight(
rLayoutWidth: String?, rLayoutHeight: String?,
parentWidth: Int, parentHeight: Int,
rLayoutWidthExclude: Int = 0, rLayoutHeightExclude: Int = 0
): IntArray {
val size = intArrayOf(-1, -1)
if (TextUtils.isEmpty(rLayoutWidth) && TextUtils.isEmpty(rLayoutHeight)) {
return size
}
if (!TextUtils.isEmpty(rLayoutWidth)) {
if (rLayoutWidth!!.contains("sw", true)) {
val ratio = rLayoutWidth.replace("sw", "", true).toFloatOrNull()
ratio?.let {
size[0] = (ratio * (screenWidth - rLayoutWidthExclude)).toInt()
}
} else if (rLayoutWidth!!.contains("pw", true)) {
val ratio = rLayoutWidth.replace("pw", "", true).toFloatOrNull()
ratio?.let {
size[0] = (ratio * (parentWidth - rLayoutWidthExclude)).toInt()
}
}
}
if (!TextUtils.isEmpty(rLayoutHeight)) {
if (rLayoutHeight!!.contains("sh", true)) {
val ratio = rLayoutHeight.replace("sh", "", true).toFloatOrNull()
ratio?.let {
size[1] = (ratio * (screenHeight - rLayoutHeightExclude)).toInt()
}
} else if (rLayoutHeight!!.contains("ph", true)) {
val ratio = rLayoutHeight.replace("ph", "", true).toFloatOrNull()
ratio?.let {
size[1] = (ratio * (parentHeight - rLayoutHeightExclude)).toInt()
}
}
}
return size
}
internal fun evaluateColor(fraction: Float /*0-1*/, startColor: Int, endColor: Int): Int {
val fr = MathUtils.clamp(fraction, 0f, 1f)
val startA = startColor shr 24 and 0xff
val startR = startColor shr 16 and 0xff
val startG = startColor shr 8 and 0xff
val startB = startColor and 0xff
val endA = endColor shr 24 and 0xff
val endR = endColor shr 16 and 0xff
val endG = endColor shr 8 and 0xff
val endB = endColor and 0xff
return startA + (fr * (endA - startA)).toInt() shl 24 or
(startR + (fr * (endR - startR)).toInt() shl 16) or
(startG + (fr * (endG - startG)).toInt() shl 8) or
startB + (fr * (endB - startB)).toInt()
}
internal fun Drawable?.tintDrawableColor(color: Int): Drawable? {
if (this == null) {
return this
}
val wrappedDrawable =
DrawableCompat.wrap(this).mutate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
DrawableCompat.setTint(wrappedDrawable, color)
} else {
wrappedDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
}
return wrappedDrawable
}
internal fun View?.tintDrawableColor(color: Int) {
when (this) {
is TextView -> {
val drawables = arrayOfNulls<Drawable?>(4)
compoundDrawables.forEachIndexed { index, drawable ->
drawables[index] = drawable?.tintDrawableColor(color)
}
setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3])
}
is ImageView -> {
setImageDrawable(drawable?.tintDrawableColor(color))
}
}
}
internal fun Paint?.textWidth(text: String?): Float {
if (TextUtils.isEmpty(text)) {
return 0f
}
return this?.run {
measureText(text)
} ?: 0f
}
internal fun Paint?.textHeight(): Float = this?.run { descent() - ascent() } ?: 0f
internal fun View.getChildOrNull(index: Int): View? {
return if (this is ViewGroup) {
return if (index in 0 until childCount) {
getChildAt(index)
} else {
null
}
} else {
this
}
}
/**获取[View]在指定[parent]中的矩形坐标*/
internal fun View.getLocationInParent(parentView: View? = null, result: Rect = Rect()): Rect {
val parent: View? = parentView ?: (parent as? View)
if (parent == null) {
getViewRect(result)
} else {
result.set(0, 0, 0, 0)
if (this != parent) {
fun doIt(view: View, parent: View, rect: Rect) {
val viewParent = view.parent
if (viewParent is View) {
rect.left += view.left
rect.top += view.top
if (viewParent != parent) {
doIt(viewParent, parent, rect)
}
}
}
doIt(this, parent, result)
}
result.right = result.left + this.measuredWidth
result.bottom = result.top + this.measuredHeight
}
return result
}
/**
* 获取View, 相对于手机屏幕的矩形
* */
internal fun View.getViewRect(result: Rect = Rect()): Rect {
var offsetX = 0
var offsetY = 0
//横屏, 并且显示了虚拟导航栏的时候. 需要左边偏移
//只计算一次
(context as? Activity)?.let {
it.window.decorView.getGlobalVisibleRect(result)
if (result.width() > result.height()) {
//横屏了
offsetX = navBarHeight(it)
}
}
return getViewRect(offsetX, offsetY, result)
}
/**
* 获取View, 相对于手机屏幕的矩形, 带皮阿尼一
* */
internal fun View.getViewRect(offsetX: Int, offsetY: Int, result: Rect = Rect()): Rect {
//可见位置的坐标, 超出屏幕的距离会被剃掉
//getGlobalVisibleRect(r)
val r2 = IntArray(2)
//val r3 = IntArray(2)
//相对于屏幕的坐标
getLocationOnScreen(r2)
//相对于窗口的坐标
//getLocationInWindow(r3)
val left = r2[0] + offsetX
val top = r2[1] + offsetY
result.set(left, top, left + measuredWidth, top + measuredHeight)
return result
}
/**
* 导航栏的高度(如果显示了)
*/
internal fun navBarHeight(context: Context): Int {
var result = 0
if (context is Activity) {
val decorRect = Rect()
val windowRect = Rect()
context.window.decorView.getGlobalVisibleRect(decorRect)
context.window.findViewById<View>(Window.ID_ANDROID_CONTENT)
.getGlobalVisibleRect(windowRect)
if (decorRect.width() > decorRect.height()) {
//横屏
result = decorRect.width() - windowRect.width()
} else {
//竖屏
result = decorRect.bottom - windowRect.bottom
}
}
return result
}
fun Collection<*>?.size() = this?.size ?: 0
/**判断2个列表中的数据是否改变过*/
internal fun <T> List<T>?.isChange(other: List<T>?): Boolean {
if (this.size() != other.size()) {
return true
}
this?.forEachIndexed { index, t ->
if (t != other?.getOrNull(index)) {
return true
}
}
return false
}
fun Int.isHorizontal() = this == LinearLayout.HORIZONTAL
fun Int.isVertical() = this == LinearLayout.VERTICAL
internal fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = true): View {
if (layoutId == -1) {
return this
}
val rootView = LayoutInflater.from(context).inflate(layoutId, this, false)
if (attachToRoot) {
addView(rootView)
}
return rootView
}

View File

@ -0,0 +1,21 @@
package com.angcyo.tablayout
/**
* 不依赖ViewPager和ViewPager2
* Email:angcyo@126.com
* @author angcyo
* @date 2019/12/14
*/
interface ViewPagerDelegate {
companion object {
const val SCROLL_STATE_IDLE = 0
const val SCROLL_STATE_DRAGGING = 1
const val SCROLL_STATE_SETTLING = 2
}
/**获取当前页面索引*/
fun onGetCurrentItem(): Int
/**设置当前的页面*/
fun onSetCurrentItem(fromIndex: Int, toIndex: Int, reselect: Boolean, fromUser: Boolean)
}

View File

@ -0,0 +1,299 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DslTabLayout">
<!--Item是否等宽-->
<attr name="tab_item_is_equ_width" format="boolean" />
<!--当子Item数量大于等于指定数量时,开启等宽,此属性优先级最高-->
<attr name="tab_item_equ_width_count" format="integer" />
<!--[~3] 小于等于3个 [3~] 大于等于3个 [3~5] 3<= <=5 -->
<attr name="tab_item_equ_width_count_range" format="string" />
<!--智能判断Item是否等宽, 如果所有子项, 未撑满tab时, 开启等宽模式.此属性会覆盖[tab_item_is_equ_width]-->
<attr name="tab_item_auto_equ_width" format="boolean" />
<!--默认选中的索引值-->
<attr name="tab_default_index" format="integer" />
<!--等宽模式下, 指定item的宽度. 不指定则平分-->
<attr name="tab_item_width" format="dimension" />
<!--在TabLayout wrap_content时, child match_parent时的高度-->
<attr name="tab_item_default_height" format="dimension" />
<!--是否绘制边框-->
<attr name="tab_draw_border" format="boolean" />
<!--是否绘制分割线-->
<attr name="tab_draw_divider" format="boolean" />
<!--是否绘制指示器-->
<attr name="tab_draw_indicator" format="boolean" />
<!--高凸模式下的背景drawable-->
<attr name="tab_convex_background" format="reference|color" />
<!--是否激活滑动选择模式-->
<attr name="tab_enable_selector_mode" format="boolean" />
<!--方向-->
<attr name="tab_orientation" format="enum">
<enum name="VERTICAL" value="1" />
<enum name="HORIZONTAL" value="0" />
</attr>
<attr name="tab_layout_scroll_anim" format="boolean" />
<attr name="tab_scroll_anim_duration" format="integer" />
<!--预览的布局id-->
<attr name="tab_preview_item_layout_id" format="reference" />
<!--预览的布局数量-->
<attr name="tab_preview_item_count" format="integer" />
<!--indicator 指示器相关属性-->
<!--强制指定指示器的Drawable-->
<attr name="tab_indicator_drawable" format="reference" />
<!--强制指定Drawable的颜色-->
<attr name="tab_indicator_color" format="color" />
<!--指示器的绘制类型, 可以使用[STYLE_TOP|STYLE_FOREGROUND]组合配置-->
<attr name="tab_indicator_style" format="flags">
<!--不绘制-->
<flag name="STYLE_NONE" value="0" />
<flag name="STYLE_TOP" value="0x1" />
<flag name="STYLE_BOTTOM" value="0x2" />
<flag name="STYLE_CENTER" value="0x4" />
<!--前景绘制-->
<flag name="STYLE_FOREGROUND" value="0x1000" />
</attr>
<!--指示器的重力-->
<attr name="tab_indicator_gravity" format="enum">
<!--指示器靠左显示-->
<enum name="GRAVITY_START" value="0x1" />
<!--指示器靠右显示-->
<enum name="GRAVITY_END" value="0x2" />
<!--指示器居中显示-->
<enum name="GRAVITY_CENTER" value="0x4" />
</attr>
<!--是否激活流式效果, ViewPager在滚动时, 指示器的宽度由小变大,再由大变小-->
<attr name="tab_indicator_enable_flow" format="boolean" />
<!--闪现效果-->
<attr name="tab_indicator_enable_flash" format="boolean" />
<!--闪现效果使用clip处理-->
<attr name="tab_indicator_enable_flash_clip" format="boolean" />
<!--tab child的索引相差多少值时, 才开启flow效果-->
<attr name="tab_indicator_flow_step" format="integer" />
<!--指示器的宽度-->
<attr name="tab_indicator_width" format="dimension|flags">
<flag name="WRAP_CONTENT" value="-2" />
<flag name="MATCH_PARENT" value="-1" />
</attr>
<!--宽度的补偿-->
<attr name="tab_indicator_width_offset" format="dimension" />
<!--同上-->
<attr name="tab_indicator_height" format="dimension|flags">
<flag name="WRAP_CONTENT" value="-2" />
<flag name="MATCH_PARENT" value="-1" />
</attr>
<!--同上-->
<attr name="tab_indicator_height_offset" format="dimension" />
<!--x轴的补偿-->
<attr name="tab_indicator_x_offset" format="dimension" />
<!--y轴的补偿, 会根据[tab_indicator_style]的类型, 自动取负值.-->
<attr name="tab_indicator_y_offset" format="dimension" />
<!--指示器child的锚点索引, 用来辅助定位计算指示器的宽度,高度,中点坐标-->
<attr name="tab_indicator_content_index" format="integer" />
<!--指示器child的锚点控件id, 用来辅助定位计算指示器的宽度,高度,中点坐标-->
<attr name="tab_indicator_content_id" format="reference" />
<!--切换指示器时, 是否需要动画的支持-->
<attr name="tab_indicator_anim" format="boolean" />
<!--请参考[GradientDrawable](https://developer.android.google.cn/reference/android/graphics/drawable/GradientDrawable)-->
<attr name="tab_indicator_shape" format="enum">
<enum name="RECTANGLE" value="0" />
<enum name="OVAL" value="1" />
<enum name="LINE" value="2" />
<enum name="RING" value="3" />
</attr>
<attr name="tab_indicator_solid_color" format="color" />
<attr name="tab_indicator_stroke_color" format="color" />
<attr name="tab_indicator_stroke_width" format="dimension" />
<attr name="tab_indicator_dash_width" format="dimension" />
<attr name="tab_indicator_dash_gap" format="dimension" />
<attr name="tab_indicator_radius" format="dimension" />
<attr name="tab_indicator_radii" format="string" />
<attr name="tab_indicator_gradient_colors" format="string" />
<attr name="tab_indicator_gradient_start_color" format="color" />
<attr name="tab_indicator_gradient_end_color" format="color" />
<attr name="tab_indicator_ignore_child_padding" format="boolean" />
<!--end...-->
<!--TabLayoutConfig 相关属性-->
<!--item选中时 文本的颜色-->
<attr name="tab_select_color" format="color" />
<!--item未选中时 文本的颜色-->
<attr name="tab_deselect_color" format="color" />
<!--选中时 图标(Drawable)的颜色, 默认是文本的颜色-->
<attr name="tab_ico_select_color" format="color" />
<!--未选中时 图标(Drawable)的颜色, 默认是文本的颜色-->
<attr name="tab_ico_deselect_color" format="color" />
<!--是否激活自动设置文本的颜色-->
<attr name="tab_enable_text_color" format="boolean" />
<!--是否激活自动设置图标的颜色-->
<attr name="tab_enable_ico_color" format="boolean" />
<!--是否激活文本变粗-->
<attr name="tab_enable_text_bold" format="boolean" />
<!--是否使用字体的方式设置变粗效果, 需要先开启[tab_enable_text_bold]-->
<attr name="tab_use_typeface_bold" format="boolean" />
<!--是否激活文本颜色渐变-->
<attr name="tab_enable_gradient_color" format="boolean" />
<!--是否激活指示器的颜色渐变效果-->
<attr name="tab_enable_indicator_gradient_color" format="boolean" />
<!--是否激活图标颜色渐变-->
<attr name="tab_enable_ico_gradient_color" format="boolean" />
<!--是否激活缩放渐变-->
<attr name="tab_enable_gradient_scale" format="boolean" />
<!--缩放渐变的最小值-->
<attr name="tab_min_scale" format="float" />
<!--缩放渐变的最大值-->
<attr name="tab_max_scale" format="float" />
<!--是否激活文本大小渐变-->
<attr name="tab_enable_gradient_text_size" format="boolean" />
<!--文本字体大小最小值-->
<attr name="tab_text_min_size" format="dimension" />
<!--文本字体大小最大值-->
<attr name="tab_text_max_size" format="dimension" />
<!--指定文本控件的id, 所有文本属性改变, 将会发生在这个控件上, 如果指定的控件不存在, 控件会降权至[ItemView]-->
<attr name="tab_text_view_id" format="reference" />
<!--指定图标控件的id, 同上-->
<attr name="tab_icon_view_id" format="reference" />
<!--end...-->
<!--Divider 分割线相关属性-->
<!--强制指定分割线的Drawable-->
<attr name="tab_divider_drawable" format="reference" />
<!--参考[GradientDrawable](https://developer.android.google.cn/reference/android/graphics/drawable/GradientDrawable)-->
<attr name="tab_divider_stroke_color" format="color" />
<attr name="tab_divider_solid_color" format="color" />
<attr name="tab_divider_stroke_width" format="dimension" />
<attr name="tab_divider_radius_size" format="dimension" />
<!--分割线margin距离-->
<attr name="tab_divider_margin_left" format="dimension" />
<attr name="tab_divider_margin_right" format="dimension" />
<attr name="tab_divider_margin_top" format="dimension" />
<attr name="tab_divider_margin_bottom" format="dimension" />
<!--分割线的宽度-->
<attr name="tab_divider_width" format="dimension" />
<attr name="tab_divider_height" format="dimension" />
<!--分割线显示的位置-->
<attr name="tab_divider_show_mode" format="flags">
<flag name="SHOW_DIVIDER_BEGINNING" value="1" />
<flag name="SHOW_DIVIDER_MIDDLE" value="2" />
<flag name="SHOW_DIVIDER_END" value="4" />
</attr>
<!--end...-->
<!--Border 边框相关属性-->
<!--强制指定边框的Drawable-->
<attr name="tab_border_drawable" format="reference" />
<!--参考[GradientDrawable](https://developer.android.google.cn/reference/android/graphics/drawable/GradientDrawable)-->
<attr name="tab_border_stroke_color" format="color" />
<attr name="tab_border_solid_color" format="color" />
<attr name="tab_border_stroke_width" format="dimension" />
<attr name="tab_border_radius_size" format="dimension" />
<!--边框是否要负责绘制item的背景, 可以自动根据边框的圆角自动配置给item-->
<attr name="tab_border_draw_item_background" format="boolean" />
<!--高度补偿-->
<attr name="tab_border_item_background_height_offset" format="dimension" />
<!--宽度补偿-->
<attr name="tab_border_item_background_width_offset" format="dimension" />
<attr name="tab_border_keep_item_radius" format="boolean" />
<attr name="tab_border_item_background_solid_color" format="color" />
<attr name="tab_border_item_background_solid_disable_color" format="color" />
<attr name="tab_border_item_background_gradient_start_color" format="color" />
<attr name="tab_border_item_background_gradient_end_color" format="color" />
<!--end...-->
<!--Badge 角标相关属性-->
<!--是否绘制角标-->
<attr name="tab_draw_badge" format="boolean" />
<!--角标的背景填充颜色-->
<attr name="tab_badge_solid_color" format="color" />
<!--角标文本的颜色-->
<attr name="tab_badge_text_color" format="color" />
<!--圆点状态时的半径大小-->
<attr name="tab_badge_circle_radius" format="dimension" />
<!--角标的圆角半径-->
<attr name="tab_badge_radius" format="dimension" />
<!--角标重力-->
<attr name="tab_badge_gravity" format="flags">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="center_vertical" value="0x10" />
<flag name="center_horizontal" value="0x01" />
<flag name="center" value="0x11" />
</attr>
<!--x轴方向的偏移量, 会根据[Gravity]自动取负值-->
<attr name="tab_badge_offset_x" format="dimension" />
<!--同上-->
<attr name="tab_badge_offset_y" format="dimension" />
<!--参考[View]的padding属性-->
<attr name="tab_badge_padding_left" format="dimension" />
<attr name="tab_badge_padding_right" format="dimension" />
<attr name="tab_badge_padding_top" format="dimension" />
<attr name="tab_badge_padding_bottom" format="dimension" />
<!--角标的文本内容, 多用于xml预览-->
<attr name="tab_badge_text" format="string" />
<!--角标的文本字体大小-->
<attr name="tab_badge_text_size" format="dimension" />
<!--角标[tab_badge_gravity]定位锚点-->
<attr name="tab_badge_anchor_child_index" format="integer" />
<!--是否要忽略锚点view的padding-->
<attr name="tab_badge_ignore_child_padding" format="boolean" />
<!--角标圆形状态下的单独配置的偏移-->
<attr name="tab_badge_circle_offset_x" format="dimension" />
<!--角标圆形状态下的单独配置的偏移-->
<attr name="tab_badge_circle_offset_y" format="dimension" />
<attr name="tab_badge_stroke_color" format="color" />
<attr name="tab_badge_stroke_width" format="dimension" />
<attr name="tab_badge_min_width" format="dimension|flags">
<flag name="WRAP_HEIGHT" value="-1" />
<flag name="NONE" value="-2" />
</attr>
<attr name="tab_badge_min_height" format="dimension">
<flag name="NONE" value="-2" />
</attr>
<!--Highlight 突出相关属性-->
<attr name="tab_draw_highlight" format="boolean" />
<attr name="tab_highlight_drawable" format="reference" />
<attr name="tab_highlight_width_offset" format="dimension" />
<attr name="tab_highlight_height_offset" format="dimension" />
<!--突出的宽度-->
<attr name="tab_highlight_width" format="dimension|flags">
<flag name="WRAP_CONTENT" value="-2" />
<flag name="MATCH_PARENT" value="-1" />
</attr>
<attr name="tab_highlight_height" format="dimension|flags">
<flag name="WRAP_CONTENT" value="-2" />
<flag name="MATCH_PARENT" value="-1" />
</attr>
<!--end...-->
</declare-styleable>
<declare-styleable name="DslTabLayout_Layout">
<!--支持按比例设置宽度, sw屏幕宽度 pw父宽度, 0.5sw:屏幕宽度的0.5倍, 0.3pw:父宽度的0.3倍-->
<attr name="layout_tab_width" format="string" />
<!--同上 sh, ph-->
<attr name="layout_tab_height" format="string" />
<!--高凸模式, 需要高凸的高度-->
<attr name="layout_tab_convex_height" format="dimension" />
<!--单独指定child的锚点索引-->
<attr name="layout_tab_indicator_content_index" format="integer" />
<!--单独指定child的锚点控件的id-->
<attr name="layout_tab_indicator_content_id" format="reference" />
<!--android.widget.LinearLayout.LayoutParams.weight 剩余空间所占比例-->
<attr name="layout_tab_weight" format="float" />
<!--单独配置的drawable, 可以覆盖tab中的配置-->
<attr name="layout_highlight_drawable" format="reference" />
<attr name="layout_tab_text_view_index" format="integer" />
<attr name="layout_tab_icon_view_index" format="integer" />
<attr name="layout_tab_text_view_id" format="reference" />
<attr name="layout_tab_icon_view_id" format="reference" />
</declare-styleable>
</resources>

1
ViewPager2Delegate/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,31 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion 28
targetSdkVersion 33
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
namespace 'com.angcyo.tablayout.delegate2'
}
dependencies {
//https://github.com/angcyo/DslTabLayout
implementation project(':TabLayout')
//https://mvnrepository.com/artifact/androidx.viewpager2/viewpager2
implementation 'androidx.viewpager2:viewpager2:1.0.0'
}
//apply from: "$gradleHost/master/publish.gradle"

View File

21
ViewPager2Delegate/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@ -0,0 +1,66 @@
package com.angcyo.tablayout.delegate2
import androidx.viewpager2.widget.ViewPager2
import com.angcyo.tablayout.DslTabLayout
import com.angcyo.tablayout.ViewPagerDelegate
import kotlin.math.absoluteValue
/**
* 兼容[ViewPager2]
* Email:angcyo@126.com
* @author angcyo
* @date 2019/12/14
*/
open class ViewPager2Delegate(
val viewPager: ViewPager2,
val dslTabLayout: DslTabLayout?,
val forceSmoothScroll: Boolean? = null
) : ViewPager2.OnPageChangeCallback(), ViewPagerDelegate {
companion object {
/**
* [forceSmoothScroll] null, 只有切换左右page时, 才有VP的动画, 否则没有.
* */
fun install(
viewPager: ViewPager2,
dslTabLayout: DslTabLayout?,
forceSmoothScroll: Boolean? = null
): ViewPager2Delegate {
return ViewPager2Delegate(viewPager, dslTabLayout, forceSmoothScroll)
}
}
init {
viewPager.registerOnPageChangeCallback(this)
dslTabLayout?.setupViewPager(this)
}
override fun onGetCurrentItem(): Int {
return viewPager.currentItem
}
override fun onSetCurrentItem(
fromIndex: Int,
toIndex: Int,
reselect: Boolean,
fromUser: Boolean
) {
if (fromUser) {
val smoothScroll = forceSmoothScroll ?: ((toIndex - fromIndex).absoluteValue <= 1)
viewPager.setCurrentItem(toIndex, smoothScroll)
}
}
override fun onPageScrollStateChanged(state: Int) {
dslTabLayout?.onPageScrollStateChanged(state)
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
dslTabLayout?.onPageScrolled(position, positionOffset, positionOffsetPixels)
}
override fun onPageSelected(position: Int) {
dslTabLayout?.onPageSelected(position)
}
}

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

74
app/build.gradle Normal file
View File

@ -0,0 +1,74 @@
plugins {
id 'com.android.application'
}
android {
signingConfigs {
debug {
storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
storePassword '34864394'
keyAlias 'yutou'
keyPassword '34864394'
}
yutou {
storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
storePassword '34864394'
keyAlias 'yutou'
keyPassword '34864394'
}
}
compileSdkVersion 34
defaultConfig {
applicationId "com.yutou.passmanage"
minSdkVersion 28
targetSdkVersion 34
versionCode 1
versionName "1.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
signingConfig signingConfigs.yutou
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace 'com.yutou.passmanage'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment:2.7.7'
implementation 'androidx.navigation:navigation-ui:2.7.7'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation 'com.alibaba.fastjson2:fastjson2:2.0.52'
implementation "androidx.room:room-runtime:2.6.1"
annotationProcessor "androidx.room:room-compiler:2.6.1"
implementation "androidx.biometric:biometric:1.1.0"
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
api project(path:':TabLayout')
api project(path:':ViewPager2Delegate')
api project(path:':netlibs')
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1,26 @@
package com.yutou.passmanage;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.yutou.passmanage", appContext.getPackageName());
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyPassworldManage"
android:usesCleartextTraffic="true" >
<activity
android:name=".MainActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,265 @@
package com.yutou.passmanage.Adapters;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.netlibs.http.HttpCallback;
import com.yutou.passmanage.Datas.ToolsPassword;
import com.yutou.passmanage.Datas.ToolsPasswordDao;
import com.yutou.passmanage.Interfaces.NetworkInterface;
import com.yutou.passmanage.NetUtils.NetTools;
import com.yutou.passmanage.R;
import com.yutou.passmanage.Tools.NetworkTool;
import com.yutou.passmanage.Tools.ResTools;
import com.yutou.passmanage.Tools.RoomDatabaseManager;
import com.yutou.passmanage.bean.BaseBean;
import java.util.List;
import static com.yutou.passmanage.Tools.RoomDatabaseManager.addPassword;
public class PassWordListAdapter extends RecyclerView.Adapter<PassWordListAdapter.MyViewHolder> {
private Context context;
private List<ToolsPassword> list;
ToolsPasswordDao dao;
public PassWordListAdapter(Context context, List<ToolsPassword> list) {
this.context = context;
this.list = list;
this.dao = RoomDatabaseManager.build(context).passwordDao();
}
public void setList(List<ToolsPassword> list) {
this.list = list;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.recycler_list, parent, false));
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
ToolsPassword data = list.get(holder.getAdapterPosition());
String account = ResTools.getString(context, R.string.account) + ":" + data.getAccount();
String password = ResTools.getString(context, R.string.password) + ":" + data.getPassword();
holder.name.setText(data.getTitle());
holder.user.setText(account);
holder.password.setText(password);
holder.copy.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NetTools.getApi().getPassword(data.getId()).enqueue(new HttpCallback<String>() {
@Override
public void onResponse(int code, String status, String response) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("", response);
clipboard.setPrimaryClip(clip);
Toast.makeText(context, ResTools.getString(context, R.string.toast_copy_password), Toast.LENGTH_LONG).show();
}
@Override
public void onFailure(Throwable throwable) {
}
});
}
});
holder.item_layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NetTools.getApi().getPassword(data.getId()).enqueue(new HttpCallback<String>() {
@Override
public void onResponse(int code, String status, String response) {
data.setPassword(response);
showDialog(data, holder.getAdapterPosition());
save(data);
}
@Override
public void onFailure(Throwable throwable) {
new Thread(() -> {
ToolsPassword queried = dao.queryPassword(data.getId());
if (queried != null) {
new Handler(Looper.getMainLooper())
.post(() -> showDialog(data, holder.getAdapterPosition()));
}
}).start();
}
public void save(ToolsPassword pwd) {
new Thread(new Runnable() {
@Override
public void run() {
ToolsPassword queried = dao.queryPassword(pwd.getId());
if (queried == null) {
dao.insert(pwd);
} else {
dao.update(pwd);
}
}
}).start();
}
});
}
});
}
AlertDialog removeDialog = null;
private void showDialog(ToolsPassword data, int position) {
View view = LayoutInflater.from(context.getApplicationContext()).inflate(R.layout.dialog_add_passworld, null);
Button delete = view.findViewById(R.id.delete);
EditText title, account, password, info;
title = view.findViewById(R.id.title);
account = view.findViewById(R.id.account);
password = view.findViewById(R.id.myPassword);
info = view.findViewById(R.id.info);
title.setText(data.getTitle());
account.setText(data.getAccount());
password.setText(data.getPassword());
info.setText(data.getInfo());
delete.setVisibility(View.VISIBLE);
delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle("确认删除操作")
.setMessage("确定删除这条密码吗?本操作不可反悔")
.setPositiveButton("删除", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
NetTools.getApi().removePassword(data.getId()).enqueue(new HttpCallback<BaseBean>() {
@Override
public void onResponse(int code, String status, BaseBean response) {
remove(false);
}
@Override
public void onFailure(Throwable throwable) {
remove(true);
}
void remove(boolean isError) {
if (isError) {
if (data.isUpload()) {
data.setRemove(true);
dao.update(data);
} else {
dao.delete(data);
}
} else {
dao.delete(data);
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(() -> {
dialog.dismiss();
removeDialog.dismiss();
Toast.makeText(context, "删除成功", Toast.LENGTH_LONG).show();
list.remove(position);
notifyDataSetChanged();
});
}
});
dialog.dismiss();
}
})
.setNegativeButton("取消", (dialog13, which) -> dialog13.dismiss()).create();
dialog.show();
}
});
removeDialog = new AlertDialog.Builder(context)
.setTitle("查看信息")
.setView(view)
.setPositiveButton("更新", (dialog1, which) -> {
addPassword(title.getText().toString(), account.getText().toString(), password.getText().toString(), info.getText().toString(), data.getId(), false, new NetworkInterface() {
@Override
public void httpGetData(Object data, int state) {
addDatabase(title.getText().toString(),
account.getText().toString(),
password.getText().toString(),
info.getText().toString(),
true);
}
@Override
public void httpError(Exception e) {
addDatabase(title.getText().toString(),
account.getText().toString(),
password.getText().toString(),
info.getText().toString(),
false);
}
void addDatabase(String title1, String account1, String password1, String info, boolean upload) {
new Thread(() -> {
data.setAccount(account1);
data.setTitle(title1);
data.setPassword(password1);
data.setInfo(info);
data.setUpload(upload);
dao.update(data);
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, ResTools.getString(context, R.string.toast_update), Toast.LENGTH_LONG).show();
notifyDataSetChanged();
}
});
}).start();
}
});
dialog1.dismiss();
})
.setNegativeButton("取消", (dialog12, which) -> dialog12.dismiss()).create();
removeDialog.show();
}
@Override
public int getItemCount() {
return list.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
ImageButton icon;
TextView name, user, password;
Button copy;
LinearLayout item_layout;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.icon);
name = itemView.findViewById(R.id.name);
user = itemView.findViewById(R.id.user);
password = itemView.findViewById(R.id.password);
copy = itemView.findViewById(R.id.copy);
item_layout = itemView.findViewById(R.id.item_layout);
}
}
}

View File

@ -0,0 +1,9 @@
package com.yutou.passmanage.Datas;
import android.os.Handler;
import android.os.Looper;
public class AppData {
public static Handler handler=new Handler(Looper.getMainLooper());
public static String key;
}

View File

@ -0,0 +1,134 @@
package com.yutou.passmanage.Datas;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* tools_password
* @author
*/
@Entity
public class ToolsPassword{
@PrimaryKey
private Integer id;
@ColumnInfo
private String title;
@ColumnInfo
@JSONField(name = "username")
private String account;
@ColumnInfo
private String password;
@Ignore
private String url;
@ColumnInfo
private String info;
@ColumnInfo
private Integer type;
@Ignore
private Integer uid;
@ColumnInfo
private boolean upload;
@ColumnInfo
private boolean remove;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public boolean isUpload() {
return upload;
}
public void setUpload(boolean upload) {
this.upload = upload;
}
public boolean isRemove() {
return remove;
}
public void setRemove(boolean remove) {
this.remove = remove;
}
@Override
public String toString() {
return "ToolsPassword{" +
"id=" + id +
", title='" + title + '\'' +
", account='" + account + '\'' +
", password='" + password + '\'' +
", url='" + url + '\'' +
", info='" + info + '\'' +
", type=" + type +
", uid=" + uid +
", upload=" + upload +
", remove=" + remove +
'}';
}
}

View File

@ -0,0 +1,33 @@
package com.yutou.passmanage.Datas;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface ToolsPasswordDao {
@Query("select * from ToolsPassword")
List<ToolsPassword> getAllAndRemove();
@Query("select * from ToolsPassword where remove=0")
List<ToolsPassword> getAll();
@Query("select * from ToolsPassword where title=:title and account=:account and password=:password and info=:info")
ToolsPassword isExist(String title,String account,String password,String info);
@Query("select * from ToolsPassword where id=:id")
ToolsPassword queryPassword(int id);
@Query("select * from ToolsPassword where title like :title and remove=0")
List<ToolsPassword> queryPassword(String title);
@Insert
void insert(ToolsPassword password);
@Insert
void insertAll(List<ToolsPassword> list);
@Delete
void delete(ToolsPassword password);
@Update
void update(ToolsPassword password);
@Query("DELETE FROM ToolsPassword")
void clear();
}

View File

@ -0,0 +1,38 @@
package com.yutou.passmanage.Datas;
/**
* tools_password_type
* @author
*/
public class ToolsPasswordType {
private Integer id;
private Integer uid;
private String title;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}

View File

@ -0,0 +1,15 @@
package com.yutou.passmanage.Interfaces;
import java.io.File;
/**
* 下载接口
*/
public abstract class DownloadInterface {
/**
* 下载完成
* @param oldJar
*/
public abstract void onDownloadOver(File oldJar);
public void onError(Exception e){};
}

View File

@ -0,0 +1,16 @@
package com.yutou.passmanage.Interfaces;
public abstract class NetworkInterface<T> {
/**
* 请求成功
* @param data 请求参数
* @param state http状态
*/
public void httpGetData(T data, int state){}
/**
* 请求异常
* @param e 异常
*/
public void httpError(Exception e){}
}

View File

@ -0,0 +1,339 @@
package com.yutou.passmanage;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.angcyo.tablayout.DslTabLayout;
import com.angcyo.tablayout.DslTabLayoutConfig;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.yutou.netlibs.http.HttpCallback;
import com.yutou.passmanage.Adapters.PassWordListAdapter;
import com.yutou.passmanage.Datas.AppData;
import com.yutou.passmanage.Datas.ToolsPassword;
import com.yutou.passmanage.Datas.ToolsPasswordDao;
import com.yutou.passmanage.Interfaces.NetworkInterface;
import com.yutou.passmanage.NetUtils.NetTools;
import com.yutou.passmanage.Tools.NetworkTool;
import com.yutou.passmanage.Tools.RoomDatabaseManager;
import com.yutou.passmanage.Tools.Tools;
import com.yutou.passmanage.bean.BaseBean;
import com.yutou.passmanage.bean.PasswordBean;
import com.yutou.passmanage.bean.PasswordTypeBean;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK;
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import static com.yutou.passmanage.Tools.RoomDatabaseManager.addPassword;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function4;
public class MainActivity extends AppCompatActivity {
EditText search;
ImageView clearSearch;
RecyclerView passList;
FloatingActionButton floatButton;
PassWordListAdapter adapter;
TextView authError;
DslTabLayout tabLayout;
Handler handler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
auth();
}
BiometricPrompt prompt;
void auth() {
if (BuildConfig.DEBUG) {
init();
authError.setVisibility(View.GONE);
passList.setVisibility(View.VISIBLE);
return;
}
BiometricManager manager = BiometricManager.from(this);
if (manager.canAuthenticate(BIOMETRIC_WEAK | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) {
prompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
Toast.makeText(MainActivity.this, "验证成功", Toast.LENGTH_LONG).show();
init();
authError.setVisibility(View.GONE);
passList.setVisibility(View.VISIBLE);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Toast.makeText(MainActivity.this, "验证失败,已退出", Toast.LENGTH_LONG).show();
exit();
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
System.out.println("errorCode = " + errorCode + ", errString = " + errString);
if (errorCode == 10) {
authError.setText("未授权,点击授权");
authError.setVisibility(View.VISIBLE);
passList.setVisibility(View.GONE);
} else if (errorCode == 13) {
}
}
});
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("验证指纹")
.setSubtitle("使用指纹来解锁应用")
.setAllowedAuthenticators(BIOMETRIC_WEAK | DEVICE_CREDENTIAL)
.build();
prompt.authenticate(promptInfo);
System.out.println("调用指纹");
} else {
Toast.makeText(this, "必须在支持指纹的设备上使用", Toast.LENGTH_LONG).show();
}
}
void exit() {
MainActivity.this.finish();
System.exit(0);
}
void initView() {
dao = RoomDatabaseManager.build(this).passwordDao();
adapter = new PassWordListAdapter(this, new ArrayList<>());
search = findViewById(R.id.search);
authError = findViewById(R.id.goneText);
clearSearch = findViewById(R.id.clearSearch);
passList = findViewById(R.id.passList);
floatButton = findViewById(R.id.floatButton);
tabLayout = findViewById(R.id.tabLayout);
floatButton.setOnClickListener(v -> showAddPasswordDialog());
passList.setLayoutManager(new LinearLayoutManager(this));
passList.setAdapter(adapter);
clearSearch.setOnClickListener(v -> {
search.setText("");
new Thread(this::showData).start();
});
authError.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
auth();
}
});
search.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
new Thread(() -> {
List<ToolsPassword> list;
if (Tools.isEmpty(s.toString())) {
list = dao.getAll();
} else {
list = dao.queryPassword("%" + s.toString() + "%");
}
handler.post(() -> {
adapter.setList(list);
adapter.notifyDataSetChanged();
});
}).start();
}
@Override
public void afterTextChanged(Editable s) {
}
});
tabLayout.configTabLayoutConfig(new Function1<DslTabLayoutConfig, Unit>() {
@Override
public Unit invoke(DslTabLayoutConfig dslTabLayoutConfig) {
dslTabLayoutConfig.setOnSelectItemView(new Function4<View, Integer, Boolean, Boolean, Boolean>() {
@Override
public Boolean invoke(View view, Integer integer, Boolean aBoolean, Boolean aBoolean2) {
if (aBoolean) {
PasswordTypeBean typeBean = (PasswordTypeBean) view.getTag();
NetTools.getApi().getTypePassword(typeBean.getId()).enqueue(new HttpCallback<List<ToolsPassword>>() {
@Override
public void onResponse(int code, String status, List<ToolsPassword> response) {
setShow(response);
}
@Override
public void onFailure(Throwable throwable) {
throwable.printStackTrace();
}
});
}
return false;
}
});
return null;
}
});
}
void showAddPasswordDialog() {
View view = LayoutInflater.from(getApplicationContext()).inflate(R.layout.dialog_add_passworld, null);
EditText title, account, password, info;
title = view.findViewById(R.id.title);
account = view.findViewById(R.id.account);
password = view.findViewById(R.id.myPassword);
info = view.findViewById(R.id.info);
AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
.setTitle("添加账号")
.setView(view)
.setPositiveButton("保存", (dialog1, which) -> {
addPassword(title.getText().toString(), account.getText().toString(), password.getText().toString(), info.getText().toString(), -1, true, new NetworkInterface() {
@Override
public void httpGetData(Object data, int state) {
addDatabase(title.getText().toString(),
account.getText().toString(),
password.getText().toString(),
true);
}
@Override
public void httpError(Exception e) {
addDatabase(title.getText().toString(),
account.getText().toString(),
password.getText().toString(),
false);
}
void addDatabase(String title1, String account1, String password1, boolean upload) {
new Thread(() -> {
ToolsPassword psd = new ToolsPassword();
psd.setTitle(title1);
psd.setAccount(account1);
psd.setPassword(password1);
psd.setUpload(upload);
dao.insert(psd);
showData();
}).start();
}
});
dialog1.dismiss();
})
.setNegativeButton("取消", (dialog12, which) -> dialog12.dismiss()).create();
dialog.show();
}
void init() {
AppData.key = Tools.getConfig("key", this);
if (Tools.isEmpty(AppData.key)) {
EditText key = new EditText(this);
key.setHint("激活码");
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("输入激活码")
.setView(key)
.setCancelable(false)
.setPositiveButton("确定", (dialog1, which) -> {
String k = key.getText().toString().trim();
if (Tools.isEmpty(k)) {
Toast.makeText(MainActivity.this, "激活码不能为空", Toast.LENGTH_LONG).show();
return;
}
Tools.setConfig("key", k, MainActivity.this);
AppData.key = k;
dialog1.dismiss();
initData();
}).create();
dialog.show();
} else {
initData();
}
}
ToolsPasswordDao dao;
void initData() {
NetTools.getApi().getAllTypePassword().enqueue(new HttpCallback<List<PasswordTypeBean>>() {
@Override
public void onResponse(int code, String status, List<PasswordTypeBean> response) {
for (PasswordTypeBean bean : response) {
Button tab = new Button(MainActivity.this);
tab.setText(bean.getTitle());
tab.setTag(bean);
tabLayout.addView(tab);
}
showLocalData();
}
@Override
public void onFailure(Throwable throwable) {
showLocalData();
}
});
new Thread(() -> {
if (dao.getAll().isEmpty()) {
authError.setText("列表为空,点击右下角+新增配置");
authError.setVisibility(View.VISIBLE);
passList.setVisibility(View.GONE);
}
}).start();
}
private void showLocalData() {
showData();
}
private void setShow(List<ToolsPassword> data) {
adapter.setList(data);
adapter.notifyDataSetChanged();
}
void showData() {
new Thread(new Runnable() {
@Override
public void run() {
List<ToolsPassword> list = dao.getAll();
handler.post(() -> {
adapter.setList(list);
adapter.notifyDataSetChanged();
});
}
}).start();
}
}

View File

@ -0,0 +1,48 @@
package com.yutou.passmanage.NetUtils;
import com.yutou.netlibs.http.HttpBody;
import com.yutou.passmanage.Datas.ToolsPassword;
import com.yutou.passmanage.bean.BaseBean;
import com.yutou.passmanage.bean.PasswordTypeBean;
import java.util.List;
import okhttp3.FormBody;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface NetApi {
@GET("/tools/password/type/get/list.do")
Call<HttpBody<List<PasswordTypeBean>>> getAllTypePassword();
@GET("/tools/password/get/list.do")
Call<HttpBody<List<ToolsPassword>>> getTypePassword(
@Query("type") int type
);
@GET("/tools/password/get/password.do")
Call<HttpBody<String>> getPassword(
@Query("id") int id
);
@FormUrlEncoded
@POST("/tools/password/set/add.do")
Call<HttpBody<BaseBean>> addPassword(@Field("title") String title,
@Field("account") String account,
@Field("password") String password,
@Field("info") String info);
@FormUrlEncoded
@POST("/tools/password/set/update.do")
Call<HttpBody<BaseBean>> updatePassword(@Field("id") int id,
@Field("title") String title,
@Field("account") String account,
@Field("password") String password,
@Field("info") String info);
@FormUrlEncoded
@POST("/tools/password/set/remove.do")
Call<HttpBody<BaseBean>> removePassword(@Field("id") int id);
}

View File

@ -0,0 +1,19 @@
package com.yutou.passmanage.NetUtils;
import com.yutou.netlibs.http.API;
import com.yutou.netlibs.http.BaseAPI;
import com.yutou.passmanage.Datas.AppData;
import java.util.HashMap;
public class NetTools extends API<NetApi> {
private NetTools(String url,HashMap<String,String> map) {
super(url,map);
}
public static NetApi getApi(){
HashMap<String,String> map=new HashMap<>();
map.put("token", AppData.key);
return new NetTools("https://tools.yutou233.cn",map).createAPI(NetApi.class);
}
}

View File

@ -0,0 +1,65 @@
package com.yutou.passmanage.Tools;
import android.util.Log;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.passmanage.Datas.AppData;
import com.yutou.passmanage.Interfaces.NetworkInterface;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.List;
import java.util.Set;
public class NetworkTool {
private final static String TAG = NetworkTool.class.getSimpleName();
public static class NetworkAPI {
}
private String getExtUa() {
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36";
}
public static String toGetSplice(JSONObject json) {
try {
json.put("token", AppData.key);
} catch (JSONException e) {
e.printStackTrace();
}
StringBuilder string = new StringBuilder();
Set<String> keys = json.keySet();
for (String key : keys) {
try {
string.append("&").append(key).append("=").append(URLEncoder.encode(json.getString(key), "UTF-8"));
} catch (Exception e) {
e.printStackTrace();
try {
string.append("&").append(URLEncoder.encode(key, "UTF-8")).append("=");
// string += "&" + key + "=";
} catch (Exception e1) {
string.append("&").append(key).append("=");
}
}
}
string = new StringBuilder(string.substring(1, string.length()).replaceAll(" ", ""));
return string.toString();
}
}

View File

@ -0,0 +1,9 @@
package com.yutou.passmanage.Tools;
import android.content.Context;
public class ResTools {
public static String getString(Context context,int id){
return context.getResources().getString(id);
}
}

View File

@ -0,0 +1,98 @@
package com.yutou.passmanage.Tools;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.DatabaseConfiguration;
import androidx.room.InvalidationTracker;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import com.alibaba.fastjson2.JSONObject;
import com.yutou.netlibs.http.HttpBody;
import com.yutou.passmanage.Datas.ToolsPassword;
import com.yutou.passmanage.Datas.ToolsPasswordDao;
import com.yutou.passmanage.Interfaces.NetworkInterface;
import com.yutou.passmanage.NetUtils.NetTools;
import com.yutou.passmanage.bean.BaseBean;
import retrofit2.Call;
import retrofit2.HttpException;
import retrofit2.Response;
@Database(entities = {ToolsPassword.class}, version = 2,exportSchema = false)
public abstract class RoomDatabaseManager extends RoomDatabase {
public abstract ToolsPasswordDao passwordDao();
@NonNull
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
return null;
}
@NonNull
@Override
protected InvalidationTracker createInvalidationTracker() {
return null;
}
@Override
public void clearAllTables() {
}
private static final Migration update1to2=new Migration(1,2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE ToolsPassword ADD COLUMN `info` ` TEXT` ");
}
};
public static RoomDatabaseManager build(Context context) {
return Room.databaseBuilder(context.getApplicationContext(), RoomDatabaseManager.class, "password.db")
.addMigrations(update1to2)
.build();
}
public static void addPassword(String title, String account, String password,String info,int id,boolean isAdd, NetworkInterface networkInterface) {
JSONObject json = new JSONObject();
json.put("title", title);
json.put("username", account);
json.put("password", password);
json.put("url", "");
json.put("info", info);
json.put("type", "-1");
if(id!=-1){
json.put("id",id);
}
if(isAdd) {
NetTools.getApi().addPassword(title,account,password,info).enqueue(new retrofit2.Callback<HttpBody<BaseBean>>() {
@Override
public void onResponse(Call<HttpBody<BaseBean>> call, Response<HttpBody<BaseBean>> response) {
networkInterface.httpGetData(response.body().getData(), response.body().getCode());
}
@Override
public void onFailure(Call<HttpBody<BaseBean>> call, Throwable t) {
networkInterface.httpError(new RuntimeException(t.getMessage()));
}
});
}else{
NetTools.getApi().updatePassword(id,title,account,password,info).enqueue(new retrofit2.Callback<HttpBody<BaseBean>>() {
@Override
public void onResponse(Call<HttpBody<BaseBean>> call, Response<HttpBody<BaseBean>> response) {
networkInterface.httpGetData(response.body().getData(), response.body().getCode());
}
@Override
public void onFailure(Call<HttpBody<BaseBean>> call, Throwable t) {
networkInterface.httpError(new RuntimeException(t.getMessage()));
}
});
}
}
}

View File

@ -0,0 +1,38 @@
package com.yutou.passmanage.Tools;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import java.util.List;
public class Tools {
public static boolean isEmpty(String str){
if(str==null|| str.trim().isEmpty()){
return true;
}
return false;
}
public static PackageInfo getPackageInfo(String packageName, Context context){
List<PackageInfo> list=context.getPackageManager().getInstalledPackages(0);
if(list!=null){
for (PackageInfo packageInfo : list) {
if(packageInfo.packageName.equals(packageName)){
return packageInfo;
}
}
}
return null;
}
public static List<PackageInfo> getAllPackageInfo(Context context){
return context.getPackageManager().getInstalledPackages(0);
}
public static void setConfig(String key,String value,Context context){
SharedPreferences preferences=context.getSharedPreferences("config.cfg",Context.MODE_PRIVATE);
preferences.edit().putString(key,value).apply();
}
public static String getConfig(String key,Context context){
SharedPreferences preferences=context.getSharedPreferences("config.cfg",Context.MODE_PRIVATE);
return preferences.getString(key,null);
}
}

View File

@ -0,0 +1,6 @@
package com.yutou.passmanage.bean;
import java.io.Serializable;
public class BaseBean implements Serializable {
}

View File

@ -0,0 +1,88 @@
package com.yutou.passmanage.bean;
public class PasswordBean extends BaseBean{
private int id;
private String title;
private String titlePinyin;
private String username;
private String password;
private String url;
private String info;
private int type;
private int uid;
public PasswordBean() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitlePinyin() {
return titlePinyin;
}
public void setTitlePinyin(String titlePinyin) {
this.titlePinyin = titlePinyin;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
}

View File

@ -0,0 +1,43 @@
package com.yutou.passmanage.bean;
public class PasswordTypeBean extends BaseBean{
private int id;
private String title;
private int uid;
public PasswordTypeBean() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
@Override
public String toString() {
return "PasswordTypeBean{" +
"id=" + id +
", title='" + title + '\'' +
", uid=" + uid +
'}';
}
}

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ems="10"
android:gravity="start|top"
android:hint="搜索"
android:inputType="textMultiLine"
app:layout_constraintEnd_toStartOf="@+id/clearSearch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/clearSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.angcyo.tablayout.DslTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</com.angcyo.tablayout.DslTabLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/passList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/tabLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search"/>
<TextView
android:id="@+id/goneText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text=""
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:clickable="true"
android:contentDescription="add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="15dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text="@string/title" />
<EditText
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:inputType="textPersonName"
android:hint="@string/title" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text="@string/account" />
<EditText
android:id="@+id/account"
android:layout_width="wrap_content"
android:layout_height="@dimen/edit_layout_height"
android:layout_weight="1"
android:ems="10"
android:inputType="textPersonName"
android:hint="@string/account"
android:autofillHints="name" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text="@string/password" />
<EditText
android:id="@+id/myPassword"
android:layout_width="wrap_content"
android:layout_height="@dimen/edit_layout_height"
android:layout_weight="1"
android:ems="10"
android:hint="@string/password"
android:autofillHints="name"
android:inputType="textPersonName"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="@string/info" />
<EditText
android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:layout_marginStart="10dp"
android:gravity="start|top"
android:hint="这里可以填写备注信息"
android:inputType="textMultiLine"
android:minLines="5" />
</LinearLayout>
<Button
android:id="@+id/delete"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除" />
</LinearLayout>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_weight="0"
android:visibility="gone"
app:srcCompat="@drawable/ic_launcher_foreground" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:gravity="right"
android:text="TextView" />
<TextView
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center"
android:gravity="right"
android:text="TextView" />
</LinearLayout>
<Button
android:id="@+id/copy"
android:layout_width="70dp"
android:layout_height="60dp"
android:layout_weight="0.1"
android:text="复制" />
</LinearLayout>
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
</LinearLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/app_icon_background"/>
<foreground android:drawable="@mipmap/app_icon_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/app_icon_background"/>
<foreground android:drawable="@mipmap/app_icon_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,21 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyPassworldManage" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<!-- Base application theme. -->
<style name="Base.Theme.MyPassworldManage" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="app_icon_background">#639BFF</color>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,56 @@
<resources>
<string name="app_name">密码管理器</string>
<string name="password">密码</string>
<string name="account">账号</string>
<string name="title">标题</string>
<string name="info">备注</string>
<string name="name">名字</string>
<string name="toast_copy_password">已复制密码</string>
<string name="toast_update">更新成功</string>
<!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="lorem_ipsum">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
bibendum, vel congue leo egestas.\n\n
Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
est.\n\n
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
convallis.\n\n
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
</string>
<string name="title_activity_main2">MainActivity2</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>
</resources>

View File

@ -0,0 +1,21 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyPassworldManage" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<!-- Base application theme. -->
<style name="Base.Theme.MyPassworldManage" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="edit_layout_height">48dp</dimen>
</resources>

View File

@ -0,0 +1,17 @@
package com.yutou.passmanage;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

24
build.gradle Normal file
View File

@ -0,0 +1,24 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

27
gradle.properties Normal file
View File

@ -0,0 +1,27 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
systemProp.http.proxyHost=127.0.0.1
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=7980
systemProp.http.proxyPort=7890
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

244
gradlew vendored Normal file
View File

@ -0,0 +1,244 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored Normal file
View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
netlibs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

34
netlibs/build.gradle Normal file
View File

@ -0,0 +1,34 @@
plugins {
id 'com.android.library'
}
android {
namespace 'com.yutou.netlibs'
compileSdk 34
defaultConfig {
minSdk 28
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
api 'com.squareup.okhttp3:okhttp:3.14.9'
api 'com.squareup.retrofit2:retrofit:2.3.0'
api 'com.alibaba.fastjson2:fastjson2:2.0.52'
}

Some files were not shown because too many files have changed in this diff Show More