diff --git a/FaceUnity/src/main/res/values/strings.xml b/FaceUnity/src/main/res/values/strings.xml index 050818a37..73f44cd05 100644 --- a/FaceUnity/src/main/res/values/strings.xml +++ b/FaceUnity/src/main/res/values/strings.xml @@ -334,4 +334,7 @@ Exquisite sticker Reset Custom + + No face tracking + No face or body tracking diff --git a/SVGAlibrary/.gitignore b/SVGAlibrary/.gitignore new file mode 100644 index 000000000..d0b97c6bc --- /dev/null +++ b/SVGAlibrary/.gitignore @@ -0,0 +1,2 @@ +/build +*.iml \ No newline at end of file diff --git a/SVGAlibrary/build.gradle b/SVGAlibrary/build.gradle new file mode 100644 index 000000000..6c6415e5e --- /dev/null +++ b/SVGAlibrary/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + + +android { + compileSdkVersion 28 + defaultConfig { + minSdkVersion 14 + targetSdkVersion 28 + } + compileOptions { + kotlinOptions.freeCompilerArgs += ['-module-name', "com.opensource.svgaplayer"] + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + packagingOptions { + exclude 'META-INF/ASL2.0' + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/MANIFEST.MF' + } +} + + +dependencies { + implementation 'com.squareup.wire:wire-runtime:4.4.1' +} diff --git a/SVGAlibrary/proguard-rules.pro b/SVGAlibrary/proguard-rules.pro new file mode 100644 index 000000000..5e9d5acdf --- /dev/null +++ b/SVGAlibrary/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/PonyCui_Home/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# 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 *; +#} diff --git a/SVGAlibrary/src/main/AndroidManifest.xml b/SVGAlibrary/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4edd510b0 --- /dev/null +++ b/SVGAlibrary/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt new file mode 100644 index 000000000..f600a33fc --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/IClickAreaListener.kt @@ -0,0 +1,9 @@ +package com.opensource.svgaplayer + +/** + * Created by miaojun on 2019/6/21. + * mail:1290846731@qq.com + */ +interface IClickAreaListener{ + fun onResponseArea(key : String,x0 : Int, y0 : Int, x1 : Int, y1 : Int) +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGACache.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGACache.kt new file mode 100644 index 000000000..8e7b50361 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGACache.kt @@ -0,0 +1,119 @@ +package com.opensource.svgaplayer + +import android.content.Context +import com.opensource.svgaplayer.utils.log.LogUtils +import java.io.File +import java.net.URL +import java.security.MessageDigest + +/** + * SVGA 缓存管理 + */ +object SVGACache { + enum class Type { + DEFAULT, + FILE + } + + private const val TAG = "SVGACache" + private var type: Type = Type.DEFAULT + private var cacheDir: String = "/" + get() { + if (field != "/") { + val dir = File(field) + if (!dir.exists()) { + dir.mkdirs() + } + } + return field + } + + + fun onCreate(context: Context?) { + onCreate(context, Type.DEFAULT) + } + + fun onCreate(context: Context?, type: Type) { + if (isInitialized()) return + context ?: return + cacheDir = "${context.cacheDir.absolutePath}/svga/" + File(cacheDir).takeIf { !it.exists() }?.mkdirs() + this.type = type + } + + /** + * 清理缓存 + */ + fun clearCache() { + if (!isInitialized()) { + LogUtils.error(TAG, "SVGACache is not init!") + return + } + SVGAParser.threadPoolExecutor.execute { + clearDir(cacheDir) + LogUtils.info(TAG, "Clear svga cache done!") + } + } + + // 清除目录下的所有文件 + internal fun clearDir(path: String) { + try { + val dir = File(path) + dir.takeIf { it.exists() }?.let { parentDir -> + parentDir.listFiles()?.forEach { file -> + if (!file.exists()) { + return@forEach + } + if (file.isDirectory) { + clearDir(file.absolutePath) + } + file.delete() + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "Clear svga cache path: $path fail", e) + } + } + + fun isInitialized(): Boolean { + return "/" != cacheDir && File(cacheDir).exists() + } + + fun isDefaultCache(): Boolean = type == Type.DEFAULT + + fun isCached(cacheKey: String): Boolean { + return if (isDefaultCache()) { + buildCacheDir(cacheKey) + } else { + buildSvgaFile( + cacheKey + ) + }.exists() + } + + fun buildCacheKey(str: String): String { + val messageDigest = MessageDigest.getInstance("MD5") + messageDigest.update(str.toByteArray(charset("UTF-8"))) + val digest = messageDigest.digest() + var sb = "" + for (b in digest) { + sb += String.format("%02x", b) + } + return sb + } + + fun buildCacheKey(url: URL): String = buildCacheKey(url.toString()) + + fun buildCacheDir(cacheKey: String): File { + return File("$cacheDir$cacheKey/") + } + + fun buildSvgaFile(cacheKey: String): File { + return File("$cacheDir$cacheKey.svga") + } + + fun buildAudioFile(audio: String): File { + return File("$cacheDir$audio.mp3") + } + +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGACallback.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGACallback.kt new file mode 100644 index 000000000..0e5a1c317 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGACallback.kt @@ -0,0 +1,13 @@ +package com.opensource.svgaplayer + +/** + * Created by cuiminghui on 2017/3/30. + */ +interface SVGACallback { + + fun onPause() + fun onFinished() + fun onRepeat() + fun onStep(frame: Int, percentage: Double) + +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt new file mode 100644 index 000000000..ccdb6e488 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAClickAreaListener.kt @@ -0,0 +1,9 @@ +package com.opensource.svgaplayer + +/** + * Created by miaojun on 2019/6/21. + * mail:1290846731@qq.com + */ +interface SVGAClickAreaListener{ + fun onClick(clickKey : String) +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt new file mode 100644 index 000000000..22cb8c0bf --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGADrawable.kt @@ -0,0 +1,106 @@ +package com.opensource.svgaplayer + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable +import android.widget.ImageView +import com.opensource.svgaplayer.drawer.SVGACanvasDrawer + +class SVGADrawable(val videoItem: SVGAVideoEntity, val dynamicItem: SVGADynamicEntity): Drawable() { + + constructor(videoItem: SVGAVideoEntity): this(videoItem, SVGADynamicEntity()) + + var cleared = true + internal set (value) { + if (field == value) { + return + } + field = value + invalidateSelf() + } + + var currentFrame = 0 + internal set (value) { + if (field == value) { + return + } + field = value + invalidateSelf() + } + + var scaleType: ImageView.ScaleType = ImageView.ScaleType.MATRIX + + private val drawer = SVGACanvasDrawer(videoItem, dynamicItem) + + override fun draw(canvas: Canvas?) { + if (cleared) { + return + } + canvas?.let { + drawer.drawFrame(it,currentFrame, scaleType) + } + } + + override fun setAlpha(alpha: Int) { + + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSPARENT + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + + } + + fun resume() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.resume(it) + }else{ + videoItem.soundPool?.resume(it) + } + } + } + } + + fun pause() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.pause(it) + }else{ + videoItem.soundPool?.pause(it) + } + } + } + } + + fun stop() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.stop(it) + }else{ + videoItem.soundPool?.stop(it) + } + } + } + } + + fun clear() { + videoItem.audioList.forEach { audio -> + audio.playID?.let { + if (SVGASoundManager.isInit()){ + SVGASoundManager.stop(it) + }else{ + videoItem.soundPool?.stop(it) + } + } + audio.playID = null + } + videoItem.clear() + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt new file mode 100644 index 000000000..f578c479c --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGADynamicEntity.kt @@ -0,0 +1,153 @@ +package com.opensource.svgaplayer + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.text.BoringLayout +import android.text.StaticLayout +import android.text.TextPaint +import java.net.HttpURLConnection +import java.net.URL + +/** + * Created by cuiminghui on 2017/3/30. + */ +class SVGADynamicEntity { + + internal var dynamicHidden: HashMap = hashMapOf() + + internal var dynamicImage: HashMap = hashMapOf() + + internal var dynamicText: HashMap = hashMapOf() + + internal var dynamicTextPaint: HashMap = hashMapOf() + + internal var dynamicStaticLayoutText: HashMap = hashMapOf() + + internal var dynamicBoringLayoutText: HashMap = hashMapOf() + + internal var dynamicDrawer: HashMap Boolean> = hashMapOf() + + //点击事件回调map + internal var mClickMap : HashMap = hashMapOf() + internal var dynamicIClickArea: HashMap = hashMapOf() + + internal var dynamicDrawerSized: HashMap Boolean> = hashMapOf() + + + internal var isTextDirty = false + + fun setHidden(value: Boolean, forKey: String) { + this.dynamicHidden.put(forKey, value) + } + + fun setDynamicImage(bitmap: Bitmap, forKey: String) { + this.dynamicImage.put(forKey, bitmap) + } + + fun setDynamicImage(url: String, forKey: String) { + val handler = android.os.Handler() + SVGAParser.threadPoolExecutor.execute { + (URL(url).openConnection() as? HttpURLConnection)?.let { + try { + it.connectTimeout = 20 * 1000 + it.requestMethod = "GET" + it.connect() + it.inputStream.use { stream -> + BitmapFactory.decodeStream(stream)?.let { + handler.post { setDynamicImage(it, forKey) } + } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + it.disconnect() + } catch (disconnectException: Throwable) { + // ignored here + } + } + } + } + } + + fun setDynamicText(text: String, textPaint: TextPaint, forKey: String) { + this.isTextDirty = true + this.dynamicText.put(forKey, text) + this.dynamicTextPaint.put(forKey, textPaint) + } + + fun setDynamicText(layoutText: StaticLayout, forKey: String) { + this.isTextDirty = true + this.dynamicStaticLayoutText.put(forKey, layoutText) + } + + fun setDynamicText(layoutText: BoringLayout, forKey: String) { + this.isTextDirty = true + BoringLayout.isBoring(layoutText.text,layoutText.paint)?.let { + this.dynamicBoringLayoutText.put(forKey,layoutText) + } + } + + fun setDynamicDrawer(drawer: (canvas: Canvas, frameIndex: Int) -> Boolean, forKey: String) { + this.dynamicDrawer.put(forKey, drawer) + } + + fun setClickArea(clickKey: List) { + for(itemKey in clickKey){ + dynamicIClickArea.put(itemKey,object : IClickAreaListener { + override fun onResponseArea(key: String, x0: Int, y0: Int, x1: Int, y1: Int) { + mClickMap.let { + if(it.get(key) == null){ + it.put(key, intArrayOf(x0,y0,x1,y1)) + }else{ + it.get(key)?.let { + it[0] = x0 + it[1] = y0 + it[2] = x1 + it[3] = y1 + } + } + } + } + }) + } + } + + fun setClickArea(clickKey: String) { + dynamicIClickArea.put(clickKey, object : IClickAreaListener { + override fun onResponseArea(key: String, x0: Int, y0: Int, x1: Int, y1: Int) { + mClickMap.let { + if (it.get(key) == null) { + it.put(key, intArrayOf(x0, y0, x1, y1)) + } else { + it.get(key)?.let { + it[0] = x0 + it[1] = y0 + it[2] = x1 + it[3] = y1 + } + } + } + } + }) + } + + fun setDynamicDrawerSized(drawer: (canvas: Canvas, frameIndex: Int, width: Int, height: Int) -> Boolean, forKey: String) { + this.dynamicDrawerSized.put(forKey, drawer) + } + + fun clearDynamicObjects() { + this.isTextDirty = true + this.dynamicHidden.clear() + this.dynamicImage.clear() + this.dynamicText.clear() + this.dynamicTextPaint.clear() + this.dynamicStaticLayoutText.clear() + this.dynamicBoringLayoutText.clear() + this.dynamicDrawer.clear() + this.dynamicIClickArea.clear() + this.mClickMap.clear() + this.dynamicDrawerSized.clear() + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt new file mode 100644 index 000000000..c188ccf7f --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAImageView.kt @@ -0,0 +1,329 @@ +package com.opensource.svgaplayer + +import android.animation.Animator +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.animation.LinearInterpolator +import android.widget.ImageView +import com.opensource.svgaplayer.utils.SVGARange +import com.opensource.svgaplayer.utils.log.LogUtils +import java.lang.ref.WeakReference +import java.net.URL + +/** + * Created by PonyCui on 2017/3/29. + */ +open class SVGAImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ImageView(context, attrs, defStyleAttr) { + + private val TAG = "SVGAImageView" + + enum class FillMode { + Backward, + Forward, + Clear, + } + + var isAnimating = false + private set + + var loops = 0 + + @Deprecated( + "It is recommended to use clearAfterDetached, or manually call to SVGAVideoEntity#clear." + + "If you just consider cleaning up the canvas after playing, you can use FillMode#Clear.", + level = DeprecationLevel.WARNING + ) + var clearsAfterStop = false + var clearsAfterDetached = false + var fillMode: FillMode = FillMode.Forward + var callback: SVGACallback? = null + + private var mAnimator: ValueAnimator? = null + private var mItemClickAreaListener: SVGAClickAreaListener? = null + private var mAntiAlias = true + private var mAutoPlay = true + private val mAnimatorListener = AnimatorListener(this) + private val mAnimatorUpdateListener = AnimatorUpdateListener(this) + private var mStartFrame = 0 + private var mEndFrame = 0 + + init { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + this.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + attrs?.let { loadAttrs(it) } + } + + private fun loadAttrs(attrs: AttributeSet) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SVGAImageView, 0, 0) + loops = typedArray.getInt(R.styleable.SVGAImageView_loopCount, 0) + clearsAfterStop = typedArray.getBoolean(R.styleable.SVGAImageView_clearsAfterStop, false) + clearsAfterDetached = typedArray.getBoolean(R.styleable.SVGAImageView_clearsAfterDetached, false) + mAntiAlias = typedArray.getBoolean(R.styleable.SVGAImageView_antiAlias, true) + mAutoPlay = typedArray.getBoolean(R.styleable.SVGAImageView_autoPlay, true) + typedArray.getString(R.styleable.SVGAImageView_fillMode)?.let { + when (it) { + "0" -> { + fillMode = FillMode.Backward + } + "1" -> { + fillMode = FillMode.Forward + } + "2" -> { + fillMode = FillMode.Clear + } + } + } + typedArray.getString(R.styleable.SVGAImageView_source)?.let { + parserSource(it) + } + typedArray.recycle() + } + + private fun parserSource(source: String) { + val refImgView = WeakReference(this) + val parser = SVGAParser(context) + if (source.startsWith("http://") || source.startsWith("https://")) { + parser.decodeFromURL(URL(source), createParseCompletion(refImgView)) + } else { + parser.decodeFromAssets(source, createParseCompletion(refImgView)) + } + } + + private fun createParseCompletion(ref: WeakReference): SVGAParser.ParseCompletion { + return object : SVGAParser.ParseCompletion { + override fun onComplete(videoItem: SVGAVideoEntity) { + ref.get()?.startAnimation(videoItem) + } + + override fun onError() {} + } + } + + private fun startAnimation(videoItem: SVGAVideoEntity) { + this@SVGAImageView.post { + videoItem.antiAlias = mAntiAlias + setVideoItem(videoItem) + getSVGADrawable()?.scaleType = scaleType + if (mAutoPlay) { + startAnimation() + } + } + } + + fun startAnimation() { + startAnimation(null, false) + } + + fun startAnimation(range: SVGARange?, reverse: Boolean = false) { + stopAnimation(false) + play(range, reverse) + } + + private fun play(range: SVGARange?, reverse: Boolean) { + LogUtils.info(TAG, "================ start animation ================") + val drawable = getSVGADrawable() ?: return + setupDrawable() + mStartFrame = Math.max(0, range?.location ?: 0) + val videoItem = drawable.videoItem + mEndFrame = Math.min(videoItem.frames - 1, ((range?.location ?: 0) + (range?.length ?: Int.MAX_VALUE) - 1)) + val animator = ValueAnimator.ofInt(mStartFrame, mEndFrame) + animator.interpolator = LinearInterpolator() + animator.duration = ((mEndFrame - mStartFrame + 1) * (1000 / videoItem.FPS) / generateScale()).toLong() + animator.repeatCount = if (loops <= 0) 99999 else loops - 1 + animator.addUpdateListener(mAnimatorUpdateListener) + animator.addListener(mAnimatorListener) + if (reverse) { + animator.reverse() + } else { + animator.start() + } + mAnimator = animator + } + + private fun setupDrawable() { + val drawable = getSVGADrawable() ?: return + drawable.cleared = false + drawable.scaleType = scaleType + } + + private fun getSVGADrawable(): SVGADrawable? { + return drawable as? SVGADrawable + } + + @Suppress("UNNECESSARY_SAFE_CALL") + private fun generateScale(): Double { + var scale = 1.0 + try { + val animatorClass = Class.forName("android.animation.ValueAnimator") ?: return scale + val getMethod = animatorClass.getDeclaredMethod("getDurationScale") ?: return scale + scale = (getMethod.invoke(animatorClass) as Float).toDouble() + if (scale == 0.0) { + val setMethod = animatorClass.getDeclaredMethod("setDurationScale",Float::class.java) ?: return scale + setMethod.isAccessible = true + setMethod.invoke(animatorClass,1.0f) + scale = 1.0 + LogUtils.info(TAG, + "The animation duration scale has been reset to" + + " 1.0x, because you closed it on developer options.") + } + } catch (ignore: Exception) { + ignore.printStackTrace() + } + return scale + } + + private fun onAnimatorUpdate(animator: ValueAnimator?) { + val drawable = getSVGADrawable() ?: return + drawable.currentFrame = animator?.animatedValue as Int + val percentage = (drawable.currentFrame + 1).toDouble() / drawable.videoItem.frames.toDouble() + callback?.onStep(drawable.currentFrame, percentage) + } + + private fun onAnimationEnd(animation: Animator?) { + isAnimating = false + stopAnimation() + val drawable = getSVGADrawable() + if (drawable != null) { + when (fillMode) { + FillMode.Backward -> { + drawable.currentFrame = mStartFrame + } + FillMode.Forward -> { + drawable.currentFrame = mEndFrame + } + FillMode.Clear -> { + drawable.cleared = true + } + } + } + callback?.onFinished() + } + + fun clear() { + getSVGADrawable()?.cleared = true + getSVGADrawable()?.clear() + // 清除对 drawable 的引用 + setImageDrawable(null) + } + + fun pauseAnimation() { + stopAnimation(false) + callback?.onPause() + } + + fun stopAnimation() { + stopAnimation(clear = clearsAfterStop) + } + + fun stopAnimation(clear: Boolean) { + mAnimator?.cancel() + mAnimator?.removeAllListeners() + mAnimator?.removeAllUpdateListeners() + getSVGADrawable()?.stop() + getSVGADrawable()?.cleared = clear + } + + fun setVideoItem(videoItem: SVGAVideoEntity?) { + setVideoItem(videoItem, SVGADynamicEntity()) + } + + fun setVideoItem(videoItem: SVGAVideoEntity?, dynamicItem: SVGADynamicEntity?) { + if (videoItem == null) { + setImageDrawable(null) + } else { + val drawable = SVGADrawable(videoItem, dynamicItem ?: SVGADynamicEntity()) + drawable.cleared = true + setImageDrawable(drawable) + } + } + + fun stepToFrame(frame: Int, andPlay: Boolean) { + pauseAnimation() + val drawable = getSVGADrawable() ?: return + drawable.currentFrame = frame + if (andPlay) { + startAnimation() + mAnimator?.let { + it.currentPlayTime = (Math.max(0.0f, Math.min(1.0f, (frame.toFloat() / drawable.videoItem.frames.toFloat()))) * it.duration).toLong() + } + } + } + + fun stepToPercentage(percentage: Double, andPlay: Boolean) { + val drawable = drawable as? SVGADrawable ?: return + var frame = (drawable.videoItem.frames * percentage).toInt() + if (frame >= drawable.videoItem.frames && frame > 0) { + frame = drawable.videoItem.frames - 1 + } + stepToFrame(frame, andPlay) + } + + fun setOnAnimKeyClickListener(clickListener : SVGAClickAreaListener){ + mItemClickAreaListener = clickListener + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event?.action != MotionEvent.ACTION_DOWN) { + return super.onTouchEvent(event) + } + val drawable = getSVGADrawable() ?: return super.onTouchEvent(event) + for ((key, value) in drawable.dynamicItem.mClickMap) { + if (event.x >= value[0] && event.x <= value[2] && event.y >= value[1] && event.y <= value[3]) { + mItemClickAreaListener?.let { + it.onClick(key) + return true + } + } + } + + return super.onTouchEvent(event) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopAnimation(clearsAfterDetached) + if (clearsAfterDetached) { + clear() + } + } + + private class AnimatorListener(view: SVGAImageView) : Animator.AnimatorListener { + private val weakReference = WeakReference(view) + + override fun onAnimationRepeat(animation: Animator?) { + weakReference.get()?.callback?.onRepeat() + } + + override fun onAnimationEnd(animation: Animator?) { + weakReference.get()?.onAnimationEnd(animation) + } + + override fun onAnimationCancel(animation: Animator?) { + weakReference.get()?.isAnimating = false + } + + override fun onAnimationStart(animation: Animator?) { + weakReference.get()?.isAnimating = true + } + } // end of AnimatorListener + + + private class AnimatorUpdateListener(view: SVGAImageView) : ValueAnimator.AnimatorUpdateListener { + private val weakReference = WeakReference(view) + + override fun onAnimationUpdate(animation: ValueAnimator?) { + weakReference.get()?.onAnimatorUpdate(animation) + } + } // end of AnimatorUpdateListener +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAParser.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAParser.kt new file mode 100644 index 000000000..2b711ef33 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAParser.kt @@ -0,0 +1,565 @@ +package com.opensource.svgaplayer + +import android.content.Context +import android.net.http.HttpResponseCache +import android.os.Handler +import android.os.Looper +import com.opensource.svgaplayer.proto.MovieEntity +import com.opensource.svgaplayer.utils.log.LogUtils +import org.json.JSONObject +import java.io.* +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.Executors +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.Inflater +import java.util.zip.ZipInputStream + +/** + * Created by PonyCui 16/6/18. + */ +private var fileLock: Int = 0 +private var isUnzipping = false + +class SVGAParser(context: Context?) { + private var mContext = context?.applicationContext + + init { + SVGACache.onCreate(context) + } + + @Volatile + private var mFrameWidth: Int = 0 + + @Volatile + private var mFrameHeight: Int = 0 + + interface ParseCompletion { + fun onComplete(videoItem: SVGAVideoEntity) + fun onError() + } + + interface PlayCallback{ + fun onPlay(file: List) + } + + open class FileDownloader { + + var noCache = false + + open fun resume(url: URL, complete: (inputStream: InputStream) -> Unit, failure: (e: Exception) -> Unit): () -> Unit { + var cancelled = false + val cancelBlock = { + cancelled = true + } + threadPoolExecutor.execute { + try { + LogUtils.info(TAG, "================ svga file download start ================") + if (HttpResponseCache.getInstalled() == null && !noCache) { + LogUtils.error(TAG, "SVGAParser can not handle cache before install HttpResponseCache. see https://github.com/yyued/SVGAPlayer-Android#cache") + LogUtils.error(TAG, "在配置 HttpResponseCache 前 SVGAParser 无法缓存. 查看 https://github.com/yyued/SVGAPlayer-Android#cache ") + } + (url.openConnection() as? HttpURLConnection)?.let { + it.connectTimeout = 20 * 1000 + it.requestMethod = "GET" + it.setRequestProperty("Connection", "close") + it.connect() + it.inputStream.use { inputStream -> + ByteArrayOutputStream().use { outputStream -> + val buffer = ByteArray(4096) + var count: Int + while (true) { + if (cancelled) { + LogUtils.warn(TAG, "================ svga file download canceled ================") + break + } + count = inputStream.read(buffer, 0, 4096) + if (count == -1) { + break + } + outputStream.write(buffer, 0, count) + } + if (cancelled) { + LogUtils.warn(TAG, "================ svga file download canceled ================") + return@execute + } + ByteArrayInputStream(outputStream.toByteArray()).use { + LogUtils.info(TAG, "================ svga file download complete ================") + complete(it) + } + } + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "================ svga file download fail ================") + LogUtils.error(TAG, "error: ${e.message}") + e.printStackTrace() + failure(e) + } + } + return cancelBlock + } + } + + var fileDownloader = FileDownloader() + + companion object { + private const val TAG = "SVGAParser" + + private val threadNum = AtomicInteger(0) + private var mShareParser = SVGAParser(null) + + internal var threadPoolExecutor = Executors.newCachedThreadPool { r -> + Thread(r, "SVGAParser-Thread-${threadNum.getAndIncrement()}") + } + + fun setThreadPoolExecutor(executor: ThreadPoolExecutor) { + threadPoolExecutor = executor + } + + fun shareParser(): SVGAParser { + return mShareParser + } + } + + fun init(context: Context) { + mContext = context.applicationContext + SVGACache.onCreate(mContext) + } + + fun setFrameSize(frameWidth: Int, frameHeight: Int) { + mFrameWidth = frameWidth + mFrameHeight = frameHeight + } + + fun decodeFromAssets( + name: String, + callback: ParseCompletion?, + playCallback: PlayCallback? = null + ) { + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return + } + LogUtils.info(TAG, "================ decode $name from assets ================") + threadPoolExecutor.execute { + try { + mContext?.assets?.open(name)?.let { + this.decodeFromInputStream( + it, + SVGACache.buildCacheKey("file:///assets/$name"), + callback, + true, + playCallback, + alias = name + ) + } + } catch (e: Exception) { + this.invokeErrorCallback(e, callback, name) + } + } + + } + + fun decodeFromURL( + url: URL, + callback: ParseCompletion?, + playCallback: PlayCallback? = null + ): (() -> Unit)? { + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return null + } + val urlPath = url.toString() + LogUtils.info(TAG, "================ decode from url: $urlPath ================") + val cacheKey = SVGACache.buildCacheKey(url); + return if (SVGACache.isCached(cacheKey)) { + LogUtils.info(TAG, "this url cached") + threadPoolExecutor.execute { + if (SVGACache.isDefaultCache()) { + this.decodeFromCacheKey(cacheKey, callback, alias = urlPath) + } else { + this.decodeFromSVGAFileCacheKey(cacheKey, callback, playCallback, alias = urlPath) + } + } + return null + } else { + LogUtils.info(TAG, "no cached, prepare to download") + fileDownloader.resume(url, { + this.decodeFromInputStream( + it, + cacheKey, + callback, + false, + playCallback, + alias = urlPath + ) + }, { + LogUtils.error( + TAG, + "================ svga file: $url download fail ================" + ) + this.invokeErrorCallback(it, callback, alias = urlPath) + }) + } + } + + /** + * 读取解析本地缓存的 svga 文件. + */ + fun decodeFromSVGAFileCacheKey( + cacheKey: String, + callback: ParseCompletion?, + playCallback: PlayCallback?, + alias: String? = null + ) { + threadPoolExecutor.execute { + try { + LogUtils.info(TAG, "================ decode $alias from svga cachel file to entity ================") + FileInputStream(SVGACache.buildSvgaFile(cacheKey)).use { inputStream -> + readAsBytes(inputStream)?.let { bytes -> + if (isZipFile(bytes)) { + this.decodeFromCacheKey(cacheKey, callback, alias) + } else { + LogUtils.info(TAG, "inflate start") + inflate(bytes)?.let { + LogUtils.info(TAG, "inflate complete") + val videoItem = SVGAVideoEntity( + MovieEntity.ADAPTER.decode(it), + File(cacheKey), + mFrameWidth, + mFrameHeight + ) + LogUtils.info(TAG, "SVGAVideoEntity prepare start") + videoItem.prepare({ + LogUtils.info(TAG, "SVGAVideoEntity prepare success") + this.invokeCompleteCallback(videoItem, callback, alias) + },playCallback) + + } ?: this.invokeErrorCallback( + Exception("inflate(bytes) cause exception"), + callback, + alias + ) + } + } ?: this.invokeErrorCallback( + Exception("readAsBytes(inputStream) cause exception"), + callback, + alias + ) + } + } catch (e: java.lang.Exception) { + this.invokeErrorCallback(e, callback, alias) + } finally { + LogUtils.info(TAG, "================ decode $alias from svga cachel file to entity end ================") + } + } + } + + fun decodeFromInputStream( + inputStream: InputStream, + cacheKey: String, + callback: ParseCompletion?, + closeInputStream: Boolean = false, + playCallback: PlayCallback? = null, + alias: String? = null + ) { + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return + } + LogUtils.info(TAG, "================ decode $alias from input stream ================") + threadPoolExecutor.execute { + try { + readAsBytes(inputStream)?.let { bytes -> + if (isZipFile(bytes)) { + LogUtils.info(TAG, "decode from zip file") + if (!SVGACache.buildCacheDir(cacheKey).exists() || isUnzipping) { + synchronized(fileLock) { + if (!SVGACache.buildCacheDir(cacheKey).exists()) { + isUnzipping = true + LogUtils.info(TAG, "no cached, prepare to unzip") + ByteArrayInputStream(bytes).use { + unzip(it, cacheKey) + isUnzipping = false + LogUtils.info(TAG, "unzip success") + } + } + } + } + this.decodeFromCacheKey(cacheKey, callback, alias) + } else { + if (!SVGACache.isDefaultCache()) { + // 如果 SVGACache 设置类型为 FILE + threadPoolExecutor.execute { + SVGACache.buildSvgaFile(cacheKey).let { cacheFile -> + try { + cacheFile.takeIf { !it.exists() }?.createNewFile() + FileOutputStream(cacheFile).write(bytes) + } catch (e: Exception) { + LogUtils.error(TAG, "create cache file fail.", e) + cacheFile.delete() + } + } + } + } + LogUtils.info(TAG, "inflate start") + inflate(bytes)?.let { + LogUtils.info(TAG, "inflate complete") + val videoItem = SVGAVideoEntity( + MovieEntity.ADAPTER.decode(it), + File(cacheKey), + mFrameWidth, + mFrameHeight + ) + LogUtils.info(TAG, "SVGAVideoEntity prepare start") + videoItem.prepare({ + LogUtils.info(TAG, "SVGAVideoEntity prepare success") + this.invokeCompleteCallback(videoItem, callback, alias) + },playCallback) + + } ?: this.invokeErrorCallback( + Exception("inflate(bytes) cause exception"), + callback, + alias + ) + } + } ?: this.invokeErrorCallback( + Exception("readAsBytes(inputStream) cause exception"), + callback, + alias + ) + } catch (e: java.lang.Exception) { + this.invokeErrorCallback(e, callback, alias) + } finally { + if (closeInputStream) { + inputStream.close() + } + LogUtils.info(TAG, "================ decode $alias from input stream end ================") + } + } + } + + /** + * @deprecated from 2.4.0 + */ + @Deprecated("This method has been deprecated from 2.4.0.", ReplaceWith("this.decodeFromAssets(assetsName, callback)")) + fun parse(assetsName: String, callback: ParseCompletion?) { + this.decodeFromAssets(assetsName, callback,null) + } + + /** + * @deprecated from 2.4.0 + */ + @Deprecated("This method has been deprecated from 2.4.0.", ReplaceWith("this.decodeFromURL(url, callback)")) + fun parse(url: URL, callback: ParseCompletion?) { + this.decodeFromURL(url, callback,null) + } + + /** + * @deprecated from 2.4.0 + */ + @Deprecated("This method has been deprecated from 2.4.0.", ReplaceWith("this.decodeFromInputStream(inputStream, cacheKey, callback, closeInputStream)")) + fun parse( + inputStream: InputStream, + cacheKey: String, + callback: ParseCompletion?, + closeInputStream: Boolean = false + ) { + this.decodeFromInputStream(inputStream, cacheKey, callback, closeInputStream,null) + } + + private fun invokeCompleteCallback( + videoItem: SVGAVideoEntity, + callback: ParseCompletion?, + alias: String? + ) { + Handler(Looper.getMainLooper()).post { + LogUtils.info(TAG, "================ $alias parser complete ================") + callback?.onComplete(videoItem) + } + } + + private fun invokeErrorCallback( + e: Exception, + callback: ParseCompletion?, + alias: String? + ) { + e.printStackTrace() + LogUtils.error(TAG, "================ $alias parser error ================") + LogUtils.error(TAG, "$alias parse error", e) + Handler(Looper.getMainLooper()).post { + callback?.onError() + } + } + + private fun decodeFromCacheKey( + cacheKey: String, + callback: ParseCompletion?, + alias: String? + ) { + LogUtils.info(TAG, "================ decode $alias from cache ================") + LogUtils.debug(TAG, "decodeFromCacheKey called with cacheKey : $cacheKey") + if (mContext == null) { + LogUtils.error(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。") + return + } + try { + val cacheDir = SVGACache.buildCacheDir(cacheKey) + File(cacheDir, "movie.binary").takeIf { it.isFile }?.let { binaryFile -> + try { + LogUtils.info(TAG, "binary change to entity") + FileInputStream(binaryFile).use { + LogUtils.info(TAG, "binary change to entity success") + this.invokeCompleteCallback( + SVGAVideoEntity( + MovieEntity.ADAPTER.decode(it), + cacheDir, + mFrameWidth, + mFrameHeight + ), + callback, + alias + ) + } + + } catch (e: Exception) { + LogUtils.error(TAG, "binary change to entity fail", e) + cacheDir.delete() + binaryFile.delete() + throw e + } + } + File(cacheDir, "movie.spec").takeIf { it.isFile }?.let { jsonFile -> + try { + LogUtils.info(TAG, "spec change to entity") + FileInputStream(jsonFile).use { fileInputStream -> + ByteArrayOutputStream().use { byteArrayOutputStream -> + val buffer = ByteArray(2048) + while (true) { + val size = fileInputStream.read(buffer, 0, buffer.size) + if (size == -1) { + break + } + byteArrayOutputStream.write(buffer, 0, size) + } + byteArrayOutputStream.toString().let { + JSONObject(it).let { + LogUtils.info(TAG, "spec change to entity success") + this.invokeCompleteCallback( + SVGAVideoEntity( + it, + cacheDir, + mFrameWidth, + mFrameHeight + ), + callback, + alias + ) + } + } + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "$alias movie.spec change to entity fail", e) + cacheDir.delete() + jsonFile.delete() + throw e + } + } + } catch (e: Exception) { + this.invokeErrorCallback(e, callback, alias) + } + } + + private fun readAsBytes(inputStream: InputStream): ByteArray? { + ByteArrayOutputStream().use { byteArrayOutputStream -> + val byteArray = ByteArray(2048) + while (true) { + val count = inputStream.read(byteArray, 0, 2048) + if (count <= 0) { + break + } else { + byteArrayOutputStream.write(byteArray, 0, count) + } + } + return byteArrayOutputStream.toByteArray() + } + } + + private fun inflate(byteArray: ByteArray): ByteArray? { + val inflater = Inflater() + inflater.setInput(byteArray, 0, byteArray.size) + val inflatedBytes = ByteArray(2048) + ByteArrayOutputStream().use { inflatedOutputStream -> + while (true) { + val count = inflater.inflate(inflatedBytes, 0, 2048) + if (count <= 0) { + break + } else { + inflatedOutputStream.write(inflatedBytes, 0, count) + } + } + inflater.end() + return inflatedOutputStream.toByteArray() + } + } + + // 是否是 zip 文件 + private fun isZipFile(bytes: ByteArray): Boolean { + return bytes.size > 4 && bytes[0].toInt() == 80 && bytes[1].toInt() == 75 && bytes[2].toInt() == 3 && bytes[3].toInt() == 4 + } + + // 解压 + private fun unzip(inputStream: InputStream, cacheKey: String) { + LogUtils.info(TAG, "================ unzip prepare ================") + val cacheDir = SVGACache.buildCacheDir(cacheKey) + cacheDir.mkdirs() + try { + BufferedInputStream(inputStream).use { + ZipInputStream(it).use { zipInputStream -> + while (true) { + val zipItem = zipInputStream.nextEntry ?: break + if (zipItem.name.contains("../")) { + // 解压路径存在路径穿越问题,直接过滤 + continue + } + if (zipItem.name.contains("/")) { + continue + } + val file = File(cacheDir, zipItem.name) + ensureUnzipSafety(file, cacheDir.absolutePath) + FileOutputStream(file).use { fileOutputStream -> + val buff = ByteArray(2048) + while (true) { + val readBytes = zipInputStream.read(buff) + if (readBytes <= 0) { + break + } + fileOutputStream.write(buff, 0, readBytes) + } + } + LogUtils.error(TAG, "================ unzip complete ================") + zipInputStream.closeEntry() + } + } + } + } catch (e: Exception) { + LogUtils.error(TAG, "================ unzip error ================") + LogUtils.error(TAG, "error", e) + SVGACache.clearDir(cacheDir.absolutePath) + cacheDir.delete() + throw e + } + } + + // 检查 zip 路径穿透 + private fun ensureUnzipSafety(outputFile: File, dstDirPath: String) { + val dstDirCanonicalPath = File(dstDirPath).canonicalPath + val outputFileCanonicalPath = outputFile.canonicalPath + if (!outputFileCanonicalPath.startsWith(dstDirCanonicalPath)) { + throw IOException("Found Zip Path Traversal Vulnerability with $dstDirCanonicalPath") + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt new file mode 100644 index 000000000..57ade038b --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAPlayer.kt @@ -0,0 +1,19 @@ +package com.opensource.svgaplayer + +import android.content.Context +import android.util.AttributeSet + +/** + * Created by cuiminghui on 2017/3/30. + * @deprecated from 2.4.0 + */ +@Deprecated("This class has been deprecated from 2.4.0. We don't recommend you to use it.") +class SVGAPlayer: SVGAImageView { + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {} + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} + +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt new file mode 100644 index 000000000..18719df02 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGASoundManager.kt @@ -0,0 +1,194 @@ +package com.opensource.svgaplayer + +/** + * @author Devin + * + * Created on 2/24/21. + */ +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.SoundPool +import android.os.Build +import com.opensource.svgaplayer.utils.log.LogUtils +import java.io.FileDescriptor + +/** + * Author : llk + * Time : 2020/10/24 + * Description : svga 音频加载管理类 + * 将 SoundPool 抽取到单例里边,规避 load 资源之后不回调 onLoadComplete 的问题。 + * + * 需要对 SVGASoundManager 进行初始化 + * + * 相关文章:Android SoundPool 崩溃问题研究 + * https://zhuanlan.zhihu.com/p/29985198 + */ +object SVGASoundManager { + + private val TAG = SVGASoundManager::class.java.simpleName + + private var soundPool: SoundPool? = null + + private val soundCallBackMap: MutableMap = mutableMapOf() + + /** + * 音量设置,范围在 [0, 1] 之间 + */ + private var volume: Float = 1f + + /** + * 音频回调 + */ + internal interface SVGASoundCallBack { + + // 音量发生变化 + fun onVolumeChange(value: Float) + + // 音频加载完成 + fun onComplete() + } + + fun init() { + init(20) + } + + fun init(maxStreams: Int) { + LogUtils.debug(TAG, "**************** init **************** $maxStreams") + if (soundPool != null) { + return + } + soundPool = getSoundPool(maxStreams) + soundPool?.setOnLoadCompleteListener { _, soundId, status -> + LogUtils.debug(TAG, "SoundPool onLoadComplete soundId=$soundId status=$status") + if (status == 0) { //加载该声音成功 + if (soundCallBackMap.containsKey(soundId)) { + soundCallBackMap[soundId]?.onComplete() + } + } + } + } + + fun release() { + LogUtils.debug(TAG, "**************** release ****************") + if (soundCallBackMap.isNotEmpty()) { + soundCallBackMap.clear() + } + } + + /** + * 根据当前播放实体,设置音量 + * + * @param volume 范围在 [0, 1] + * @param entity 根据需要控制对应 entity 音量大小,若为空则控制所有正在播放的音频音量 + */ + fun setVolume(volume: Float, entity: SVGAVideoEntity? = null) { + if (!checkInit()) { + return + } + + if (volume < 0f || volume > 1f) { + LogUtils.error(TAG, "The volume level is in the range of 0 to 1 ") + return + } + + if (entity == null) { + this.volume = volume + val iterator = soundCallBackMap.entries.iterator() + while (iterator.hasNext()) { + val e = iterator.next() + e.value.onVolumeChange(volume) + } + return + } + + val soundPool = soundPool ?: return + + entity.audioList.forEach { audio -> + val streamId = audio.playID ?: return + soundPool.setVolume(streamId, volume, volume) + } + } + + /** + * 是否初始化 + * 如果没有初始化,就使用原来SvgaPlayer库的音频加载逻辑。 + * @return true 则已初始化, 否则为 false + */ + internal fun isInit(): Boolean { + return soundPool != null + } + + private fun checkInit(): Boolean { + val isInit = isInit() + if (!isInit) { + LogUtils.error(TAG, "soundPool is null, you need call init() !!!") + } + return isInit + } + + private fun getSoundPool(maxStreams: Int) = if (Build.VERSION.SDK_INT >= 21) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + SoundPool.Builder().setAudioAttributes(attributes) + .setMaxStreams(maxStreams) + .build() + } else { + SoundPool(maxStreams, AudioManager.STREAM_MUSIC, 0) + } + + internal fun load(callBack: SVGASoundCallBack?, + fd: FileDescriptor?, + offset: Long, + length: Long, + priority: Int): Int { + if (!checkInit()) return -1 + + val soundId = soundPool!!.load(fd, offset, length, priority) + + LogUtils.debug(TAG, "load soundId=$soundId callBack=$callBack") + + if (callBack != null && !soundCallBackMap.containsKey(soundId)) { + soundCallBackMap[soundId] = callBack + } + return soundId + } + + internal fun unload(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "unload soundId=$soundId") + + soundPool!!.unload(soundId) + + soundCallBackMap.remove(soundId) + } + + internal fun play(soundId: Int): Int { + if (!checkInit()) return -1 + + LogUtils.debug(TAG, "play soundId=$soundId") + return soundPool!!.play(soundId, volume, volume, 1, 0, 1.0f) + } + + internal fun stop(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "stop soundId=$soundId") + soundPool!!.stop(soundId) + } + + internal fun resume(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "stop soundId=$soundId") + soundPool!!.resume(soundId) + } + + internal fun pause(soundId: Int) { + if (!checkInit()) return + + LogUtils.debug(TAG, "pause soundId=$soundId") + soundPool!!.pause(soundId) + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt new file mode 100644 index 000000000..49cb83027 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/SVGAVideoEntity.kt @@ -0,0 +1,347 @@ +package com.opensource.svgaplayer + +import android.graphics.Bitmap +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.SoundPool +import android.os.Build +import com.opensource.svgaplayer.bitmap.SVGABitmapByteArrayDecoder +import com.opensource.svgaplayer.bitmap.SVGABitmapFileDecoder +import com.opensource.svgaplayer.entities.SVGAAudioEntity +import com.opensource.svgaplayer.entities.SVGAVideoSpriteEntity +import com.opensource.svgaplayer.proto.AudioEntity +import com.opensource.svgaplayer.proto.MovieEntity +import com.opensource.svgaplayer.proto.MovieParams +import com.opensource.svgaplayer.utils.SVGARect +import com.opensource.svgaplayer.utils.log.LogUtils +import org.json.JSONObject +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.* +import kotlin.collections.ArrayList + +/** + * Created by PonyCui on 16/6/18. + */ +class SVGAVideoEntity { + + private val TAG = "SVGAVideoEntity" + + var antiAlias = true + var movieItem: MovieEntity? = null + + var videoSize = SVGARect(0.0, 0.0, 0.0, 0.0) + private set + + var FPS = 15 + private set + + var frames: Int = 0 + private set + + internal var spriteList: List = emptyList() + internal var audioList: List = emptyList() + internal var soundPool: SoundPool? = null + private var soundCallback: SVGASoundManager.SVGASoundCallBack? = null + internal var imageMap = HashMap() + private var mCacheDir: File + private var mFrameHeight = 0 + private var mFrameWidth = 0 + private var mPlayCallback: SVGAParser.PlayCallback?=null + private lateinit var mCallback: () -> Unit + + constructor(json: JSONObject, cacheDir: File) : this(json, cacheDir, 0, 0) + + constructor(json: JSONObject, cacheDir: File, frameWidth: Int, frameHeight: Int) { + mFrameWidth = frameWidth + mFrameHeight = frameHeight + mCacheDir = cacheDir + val movieJsonObject = json.optJSONObject("movie") ?: return + setupByJson(movieJsonObject) + try { + parserImages(json) + } catch (e: Exception) { + e.printStackTrace() + } catch (e: OutOfMemoryError) { + e.printStackTrace() + } + resetSprites(json) + } + + private fun setupByJson(movieObject: JSONObject) { + movieObject.optJSONObject("viewBox")?.let { viewBoxObject -> + val width = viewBoxObject.optDouble("width", 0.0) + val height = viewBoxObject.optDouble("height", 0.0) + videoSize = SVGARect(0.0, 0.0, width, height) + } + FPS = movieObject.optInt("fps", 20) + frames = movieObject.optInt("frames", 0) + } + + constructor(entity: MovieEntity, cacheDir: File) : this(entity, cacheDir, 0, 0) + + constructor(entity: MovieEntity, cacheDir: File, frameWidth: Int, frameHeight: Int) { + this.mFrameWidth = frameWidth + this.mFrameHeight = frameHeight + this.mCacheDir = cacheDir + this.movieItem = entity + entity.params?.let(this::setupByMovie) + try { + parserImages(entity) + } catch (e: Exception) { + e.printStackTrace() + } catch (e: OutOfMemoryError) { + e.printStackTrace() + } + resetSprites(entity) + } + + private fun setupByMovie(movieParams: MovieParams) { + val width = (movieParams.viewBoxWidth ?: 0.0f).toDouble() + val height = (movieParams.viewBoxHeight ?: 0.0f).toDouble() + videoSize = SVGARect(0.0, 0.0, width, height) + FPS = movieParams.fps ?: 20 + frames = movieParams.frames ?: 0 + } + + internal fun prepare(callback: () -> Unit, playCallback: SVGAParser.PlayCallback?) { + mCallback = callback + mPlayCallback = playCallback + if (movieItem == null) { + mCallback() + } else { + setupAudios(movieItem!!) { + mCallback() + } + } + } + + private fun parserImages(json: JSONObject) { + val imgJson = json.optJSONObject("images") ?: return + imgJson.keys().forEach { imgKey -> + val filePath = generateBitmapFilePath(imgJson[imgKey].toString(), imgKey) + if (filePath.isEmpty()) { + return + } + val bitmapKey = imgKey.replace(".matte", "") + val bitmap = createBitmap(filePath) + if (bitmap != null) { + imageMap[bitmapKey] = bitmap + } + } + } + + private fun generateBitmapFilePath(imgName: String, imgKey: String): String { + val path = mCacheDir.absolutePath + "/" + imgName + val path1 = "$path.png" + val path2 = mCacheDir.absolutePath + "/" + imgKey + ".png" + + return when { + File(path).exists() -> path + File(path1).exists() -> path1 + File(path2).exists() -> path2 + else -> "" + } + } + + private fun createBitmap(filePath: String): Bitmap? { + return SVGABitmapFileDecoder.decodeBitmapFrom(filePath, mFrameWidth, mFrameHeight) + } + + private fun parserImages(obj: MovieEntity) { + obj.images?.entries?.forEach { entry -> + val byteArray = entry.value.toByteArray() + if (byteArray.count() < 4) { + return@forEach + } + val fileTag = byteArray.slice(IntRange(0, 3)) + if (fileTag[0].toInt() == 73 && fileTag[1].toInt() == 68 && fileTag[2].toInt() == 51) { + return@forEach + } + val filePath = generateBitmapFilePath(entry.value.utf8(), entry.key) + createBitmap(byteArray, filePath)?.let { bitmap -> + imageMap[entry.key] = bitmap + } + } + } + + private fun createBitmap(byteArray: ByteArray, filePath: String): Bitmap? { + val bitmap = SVGABitmapByteArrayDecoder.decodeBitmapFrom(byteArray, mFrameWidth, mFrameHeight) + return bitmap ?: createBitmap(filePath) + } + + private fun resetSprites(json: JSONObject) { + val mutableList: MutableList = mutableListOf() + json.optJSONArray("sprites")?.let { item -> + for (i in 0 until item.length()) { + item.optJSONObject(i)?.let { entryJson -> + mutableList.add(SVGAVideoSpriteEntity(entryJson)) + } + } + } + spriteList = mutableList.toList() + } + + private fun resetSprites(entity: MovieEntity) { + spriteList = entity.sprites?.map { + return@map SVGAVideoSpriteEntity(it) + } ?: listOf() + } + + private fun setupAudios(entity: MovieEntity, completionBlock: () -> Unit) { + if (entity.audios == null || entity.audios.isEmpty()) { + run(completionBlock) + return + } + setupSoundPool(entity, completionBlock) + val audiosFileMap = generateAudioFileMap(entity) + //repair when audioEntity error can not callback + //如果audiosFileMap为空 soundPool?.load 不会走 导致 setOnLoadCompleteListener 不会回调 导致外层prepare不回调卡住 + if (audiosFileMap.size == 0) { + run(completionBlock) + return + } + this.audioList = entity.audios.map { audio -> + return@map createSvgaAudioEntity(audio, audiosFileMap) + } + } + + private fun createSvgaAudioEntity(audio: AudioEntity, audiosFileMap: HashMap): SVGAAudioEntity { + val item = SVGAAudioEntity(audio) + val startTime = (audio.startTime ?: 0).toDouble() + val totalTime = (audio.totalTime ?: 0).toDouble() + if (totalTime.toInt() == 0) { + // 除数不能为 0 + return item + } + // 直接回调文件,后续播放都不走 + mPlayCallback?.let { + val fileList: MutableList = ArrayList() + audiosFileMap.forEach { entity -> + fileList.add(entity.value) + } + it.onPlay(fileList) + mCallback() + return item + } + + audiosFileMap[audio.audioKey]?.let { file -> + FileInputStream(file).use { + val length = it.available().toDouble() + val offset = ((startTime / totalTime) * length).toLong() + if (SVGASoundManager.isInit()) { + item.soundID = SVGASoundManager.load(soundCallback, + it.fd, + offset, + length.toLong(), + 1) + } else { + item.soundID = soundPool?.load(it.fd, offset, length.toLong(), 1) + } + } + } + return item + } + + private fun generateAudioFile(audioCache: File, value: ByteArray): File { + audioCache.createNewFile() + FileOutputStream(audioCache).write(value) + return audioCache + } + + private fun generateAudioFileMap(entity: MovieEntity): HashMap { + val audiosDataMap = generateAudioMap(entity) + val audiosFileMap = HashMap() + if (audiosDataMap.count() > 0) { + audiosDataMap.forEach { + val audioCache = SVGACache.buildAudioFile(it.key) + audiosFileMap[it.key] = + audioCache.takeIf { file -> file.exists() } ?: generateAudioFile( + audioCache, + it.value + ) + } + } + return audiosFileMap + } + + private fun generateAudioMap(entity: MovieEntity): HashMap { + val audiosDataMap = HashMap() + entity.images?.entries?.forEach { + val imageKey = it.key + val byteArray = it.value.toByteArray() + if (byteArray.count() < 4) { + return@forEach + } + val fileTag = byteArray.slice(IntRange(0, 3)) + if (fileTag[0].toInt() == 73 && fileTag[1].toInt() == 68 && fileTag[2].toInt() == 51) { + audiosDataMap[imageKey] = byteArray + }else if(fileTag[0].toInt() == -1 && fileTag[1].toInt() == -5 && fileTag[2].toInt() == -108){ + audiosDataMap[imageKey] = byteArray + } + } + return audiosDataMap + } + + private fun setupSoundPool(entity: MovieEntity, completionBlock: () -> Unit) { + var soundLoaded = 0 + if (SVGASoundManager.isInit()) { + soundCallback = object : SVGASoundManager.SVGASoundCallBack { + override fun onVolumeChange(value: Float) { + SVGASoundManager.setVolume(value, this@SVGAVideoEntity) + } + + override fun onComplete() { + soundLoaded++ + if (soundLoaded >= entity.audios.count()) { + completionBlock() + } + } + } + return + } + soundPool = generateSoundPool(entity) + LogUtils.info("SVGAParser", "pool_start") + soundPool?.setOnLoadCompleteListener { _, _, _ -> + LogUtils.info("SVGAParser", "pool_complete") + soundLoaded++ + if (soundLoaded >= entity.audios.count()) { + completionBlock() + } + } + } + + private fun generateSoundPool(entity: MovieEntity): SoundPool? { + return try { + if (Build.VERSION.SDK_INT >= 21) { + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + SoundPool.Builder().setAudioAttributes(attributes) + .setMaxStreams(12.coerceAtMost(entity.audios.count())) + .build() + } else { + SoundPool(12.coerceAtMost(entity.audios.count()), AudioManager.STREAM_MUSIC, 0) + } + } catch (e: Exception) { + LogUtils.error(TAG, e) + null + } + } + + fun clear() { + if (SVGASoundManager.isInit()) { + this.audioList.forEach { + it.soundID?.let { id -> SVGASoundManager.unload(id) } + } + soundCallback = null + } + soundPool?.release() + soundPool = null + audioList = emptyList() + spriteList = emptyList() + imageMap.clear() + } +} + diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt new file mode 100644 index 000000000..e1ea015eb --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/BitmapSampleSizeCalculator.kt @@ -0,0 +1,33 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.BitmapFactory + +/** + * + * Create by im_dsd 2020/7/7 17:59 + */ +internal object BitmapSampleSizeCalculator { + + fun calculate(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { + // Raw height and width of image + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + + if (reqHeight <= 0 || reqWidth <= 0) { + return inSampleSize + } + if (height > reqHeight || width > reqWidth) { + + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt new file mode 100644 index 000000000..0c1dba648 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapByteArrayDecoder.kt @@ -0,0 +1,16 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * 通过字节码解码 Bitmap + * + * Create by im_dsd 2020/7/7 17:50 + */ +internal object SVGABitmapByteArrayDecoder : SVGABitmapDecoder() { + + override fun onDecode(data: ByteArray, ops: BitmapFactory.Options): Bitmap? { + return BitmapFactory.decodeByteArray(data, 0, data.count(), ops) + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt new file mode 100644 index 000000000..8816fbb01 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapDecoder.kt @@ -0,0 +1,35 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * Bitmap 解码器 + * + * 需要加载的数据类型 + * + * Create by im_dsd 2020/7/7 17:39 + */ +internal abstract class SVGABitmapDecoder { + + fun decodeBitmapFrom(data: T, reqWidth: Int, reqHeight: Int): Bitmap? { + return BitmapFactory.Options().run { + // 如果期望的宽高是合法的, 则开启检测尺寸模式 + inJustDecodeBounds = (reqWidth > 0 && reqHeight > 0) + inPreferredConfig = Bitmap.Config.RGB_565 + + val bitmap = onDecode(data, this) + if (!inJustDecodeBounds) { + return bitmap + } + + // Calculate inSampleSize + inSampleSize = BitmapSampleSizeCalculator.calculate(this, reqWidth, reqHeight) + // Decode bitmap with inSampleSize set + inJustDecodeBounds = false + onDecode(data, this) + } + } + + abstract fun onDecode(data: T, ops: BitmapFactory.Options): Bitmap? +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt new file mode 100644 index 000000000..edca5bcd9 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/bitmap/SVGABitmapFileDecoder.kt @@ -0,0 +1,16 @@ +package com.opensource.svgaplayer.bitmap + +import android.graphics.Bitmap +import android.graphics.BitmapFactory + +/** + * 通过文件解码 Bitmap + * + * Create by im_dsd 2020/7/7 17:50 + */ +internal object SVGABitmapFileDecoder : SVGABitmapDecoder() { + + override fun onDecode(data: String, ops: BitmapFactory.Options): Bitmap? { + return BitmapFactory.decodeFile(data, ops) + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt new file mode 100644 index 000000000..93ad38468 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/drawer/SGVADrawer.kt @@ -0,0 +1,53 @@ +package com.opensource.svgaplayer.drawer + +import android.graphics.Canvas +import android.widget.ImageView +import com.opensource.svgaplayer.SVGAVideoEntity +import com.opensource.svgaplayer.entities.SVGAVideoSpriteFrameEntity +import com.opensource.svgaplayer.utils.Pools +import com.opensource.svgaplayer.utils.SVGAScaleInfo +import kotlin.math.max + +/** + * Created by cuiminghui on 2017/3/29. + */ + +open internal class SGVADrawer(val videoItem: SVGAVideoEntity) { + + val scaleInfo = SVGAScaleInfo() + + private val spritePool = Pools.SimplePool(max(1, videoItem.spriteList.size)) + + inner class SVGADrawerSprite(var _matteKey: String? = null, var _imageKey: String? = null, var _frameEntity: SVGAVideoSpriteFrameEntity? = null) { + val matteKey get() = _matteKey + val imageKey get() = _imageKey + val frameEntity get() = _frameEntity!! + } + + internal fun requestFrameSprites(frameIndex: Int): List { + return videoItem.spriteList.mapNotNull { + if (frameIndex >= 0 && frameIndex < it.frames.size) { + it.imageKey?.let { imageKey -> + if (!imageKey.endsWith(".matte") && it.frames[frameIndex].alpha <= 0.0) { + return@mapNotNull null + } + return@mapNotNull (spritePool.acquire() ?: SVGADrawerSprite()).apply { + _matteKey = it.matteKey + _imageKey = it.imageKey + _frameEntity = it.frames[frameIndex] + } + } + } + return@mapNotNull null + } + } + + internal fun releaseFrameSprites(sprites: List) { + sprites.forEach { spritePool.release(it) } + } + + open fun drawFrame(canvas : Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) { + scaleInfo.performScaleType(canvas.width.toFloat(),canvas.height.toFloat(), videoItem.videoSize.width.toFloat(), videoItem.videoSize.height.toFloat(), scaleType) + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt new file mode 100644 index 000000000..42a0fbdee --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/drawer/SVGACanvasDrawer.kt @@ -0,0 +1,559 @@ +package com.opensource.svgaplayer.drawer + +import android.graphics.* +import android.os.Build +import android.text.StaticLayout +import android.text.TextUtils +import android.widget.ImageView +import com.opensource.svgaplayer.SVGADynamicEntity +import com.opensource.svgaplayer.SVGASoundManager +import com.opensource.svgaplayer.SVGAVideoEntity +import com.opensource.svgaplayer.entities.SVGAVideoShapeEntity + +/** + * Created by cuiminghui on 2017/3/29. + */ + +internal class SVGACanvasDrawer(videoItem: SVGAVideoEntity, val dynamicItem: SVGADynamicEntity) : SGVADrawer(videoItem) { + + private val sharedValues = ShareValues() + private val drawTextCache: HashMap = hashMapOf() + private val pathCache = PathCache() + + private var beginIndexList: Array? = null + private var endIndexList: Array? = null + + override fun drawFrame(canvas: Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) { + super.drawFrame(canvas, frameIndex, scaleType) + playAudio(frameIndex) + this.pathCache.onSizeChanged(canvas) + val sprites = requestFrameSprites(frameIndex) + // Filter null sprites + if (sprites.count() <= 0) return + val matteSprites = mutableMapOf() + var saveID = -1 + beginIndexList = null + endIndexList = null + + // Filter no matte layer + var hasMatteLayer = false + sprites.get(0).imageKey?.let { + if (it.endsWith(".matte")) { + hasMatteLayer = true + } + } + sprites.forEachIndexed { index, svgaDrawerSprite -> + + // Save matte sprite + svgaDrawerSprite.imageKey?.let { + /// No matte layer included or VERSION Unsopport matte + if (!hasMatteLayer || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // Normal sprite + drawSprite(svgaDrawerSprite, canvas, frameIndex) + // Continue + return@forEachIndexed + } + /// Cache matte sprite + if (it.endsWith(".matte")) { + matteSprites.put(it, svgaDrawerSprite) + // Continue + return@forEachIndexed + } + } + /// Is matte begin + if (isMatteBegin(index, sprites)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + saveID = canvas.saveLayer(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), null) + } else { + canvas.save() + } + } + /// Normal matte + drawSprite(svgaDrawerSprite, canvas, frameIndex) + + /// Is matte end + if (isMatteEnd(index, sprites)) { + matteSprites.get(svgaDrawerSprite.matteKey)?.let { + drawSprite(it, this.sharedValues.shareMatteCanvas(canvas.width, canvas.height), frameIndex) + canvas.drawBitmap(this.sharedValues.sharedMatteBitmap(), 0f, 0f, this.sharedValues.shareMattePaint()) + if (saveID != -1) { + canvas.restoreToCount(saveID) + } else { + canvas.restore() + } + // Continue + return@forEachIndexed + } + } + } + releaseFrameSprites(sprites) + } + + private fun isMatteBegin(spriteIndex: Int, sprites: List): Boolean { + if (beginIndexList == null) { + val boolArray = Array(sprites.count()) { false } + sprites.forEachIndexed { index, svgaDrawerSprite -> + svgaDrawerSprite.imageKey?.let { + /// Filter matte sprite + if (it.endsWith(".matte")) { + // Continue + return@forEachIndexed + } + } + svgaDrawerSprite.matteKey?.let { + if (it.length > 0) { + sprites.get(index - 1)?.let { lastSprite -> + if (lastSprite.matteKey.isNullOrEmpty()) { + boolArray[index] = true + } else { + if (lastSprite.matteKey != svgaDrawerSprite.matteKey) { + boolArray[index] = true + } + } + } + } + } + } + beginIndexList = boolArray + } + return beginIndexList?.get(spriteIndex) ?: false + } + + private fun isMatteEnd(spriteIndex: Int, sprites: List): Boolean { + if (endIndexList == null) { + val boolArray = Array(sprites.count()) { false } + sprites.forEachIndexed { index, svgaDrawerSprite -> + svgaDrawerSprite.imageKey?.let { + /// Filter matte sprite + if (it.endsWith(".matte")) { + // Continue + return@forEachIndexed + } + } + svgaDrawerSprite.matteKey?.let { + if (it.length > 0) { + // Last one + if (index == sprites.count() - 1) { + boolArray[index] = true + } else { + sprites.get(index + 1)?.let { nextSprite -> + if (nextSprite.matteKey.isNullOrEmpty()) { + boolArray[index] = true + } else { + if (nextSprite.matteKey != svgaDrawerSprite.matteKey) { + boolArray[index] = true + } + } + } + } + } + } + } + endIndexList = boolArray + } + return endIndexList?.get(spriteIndex) ?: false + } + + private fun playAudio(frameIndex: Int) { + this.videoItem.audioList.forEach { audio -> + if (audio.startFrame == frameIndex) { + if (SVGASoundManager.isInit()) { + audio.soundID?.let { soundID -> + audio.playID = SVGASoundManager.play(soundID) + } + } else { + this.videoItem.soundPool?.let { soundPool -> + audio.soundID?.let { soundID -> + audio.playID = soundPool.play(soundID, 1.0f, 1.0f, 1, 0, 1.0f) + } + } + } + + } + if (audio.endFrame <= frameIndex) { + audio.playID?.let { + if (SVGASoundManager.isInit()) { + SVGASoundManager.stop(it) + } else { + this.videoItem.soundPool?.stop(it) + } + } + audio.playID = null + } + } + } + + private fun shareFrameMatrix(transform: Matrix): Matrix { + val matrix = this.sharedValues.sharedMatrix() + matrix.postScale(scaleInfo.scaleFx, scaleInfo.scaleFy) + matrix.postTranslate(scaleInfo.tranFx, scaleInfo.tranFy) + matrix.preConcat(transform) + return matrix + } + + private fun drawSprite(sprite: SVGADrawerSprite, canvas: Canvas, frameIndex: Int) { + drawImage(sprite, canvas) + drawShape(sprite, canvas) + drawDynamic(sprite, canvas, frameIndex) + } + + private fun drawImage(sprite: SVGADrawerSprite, canvas: Canvas) { + val imageKey = sprite.imageKey ?: return + val isHidden = dynamicItem.dynamicHidden[imageKey] == true + if (isHidden) { + return + } + val bitmapKey = if (imageKey.endsWith(".matte")) imageKey.substring(0, imageKey.length - 6) else imageKey + val drawingBitmap = (dynamicItem.dynamicImage[bitmapKey] ?: videoItem.imageMap[bitmapKey]) + ?: return + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + val paint = this.sharedValues.sharedPaint() + paint.isAntiAlias = videoItem.antiAlias + paint.isFilterBitmap = videoItem.antiAlias + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + if (sprite.frameEntity.maskPath != null) { + val maskPath = sprite.frameEntity.maskPath ?: return + canvas.save() + val path = this.sharedValues.sharedPath() + maskPath.buildPath(path) + path.transform(frameMatrix) + canvas.clipPath(path) + frameMatrix.preScale((sprite.frameEntity.layout.width / drawingBitmap.width).toFloat(), (sprite.frameEntity.layout.height / drawingBitmap.height).toFloat()) + if (!drawingBitmap.isRecycled) { + canvas.drawBitmap(drawingBitmap, frameMatrix, paint) + } + canvas.restore() + } else { + frameMatrix.preScale((sprite.frameEntity.layout.width / drawingBitmap.width).toFloat(), (sprite.frameEntity.layout.height / drawingBitmap.height).toFloat()) + if (!drawingBitmap.isRecycled) { + canvas.drawBitmap(drawingBitmap, frameMatrix, paint) + } + } + dynamicItem.dynamicIClickArea.let { + it.get(imageKey)?.let { listener -> + val matrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) + frameMatrix.getValues(matrixArray) + listener.onResponseArea(imageKey, matrixArray[2].toInt() + , matrixArray[5].toInt() + , (drawingBitmap.width * matrixArray[0] + matrixArray[2]).toInt() + , (drawingBitmap.height * matrixArray[4] + matrixArray[5]).toInt()) + } + } + drawTextOnBitmap(canvas, drawingBitmap, sprite, frameMatrix) + } + + private fun drawTextOnBitmap(canvas: Canvas, drawingBitmap: Bitmap, sprite: SVGADrawerSprite, frameMatrix: Matrix) { + if (dynamicItem.isTextDirty) { + this.drawTextCache.clear() + dynamicItem.isTextDirty = false + } + val imageKey = sprite.imageKey ?: return + var textBitmap: Bitmap? = null + dynamicItem.dynamicText[imageKey]?.let { drawingText -> + dynamicItem.dynamicTextPaint[imageKey]?.let { drawingTextPaint -> + drawTextCache[imageKey]?.let { + textBitmap = it + } ?: kotlin.run { + textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) + val drawRect = Rect(0, 0, drawingBitmap.width, drawingBitmap.height) + val textCanvas = Canvas(textBitmap) + drawingTextPaint.isAntiAlias = true + val fontMetrics = drawingTextPaint.getFontMetrics(); + val top = fontMetrics.top + val bottom = fontMetrics.bottom + val baseLineY = drawRect.centerY() - top / 2 - bottom / 2 + textCanvas.drawText(drawingText, drawRect.centerX().toFloat(), baseLineY, drawingTextPaint); + drawTextCache.put(imageKey, textBitmap as Bitmap) + } + } + } + + dynamicItem.dynamicBoringLayoutText[imageKey]?.let { + drawTextCache[imageKey]?.let { + textBitmap = it + } ?: kotlin.run { + it.paint.isAntiAlias = true + + textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) + val textCanvas = Canvas(textBitmap) + textCanvas.translate(0f, ((drawingBitmap.height - it.height) / 2).toFloat()) + it.draw(textCanvas) + drawTextCache.put(imageKey, textBitmap as Bitmap) + } + } + + dynamicItem.dynamicStaticLayoutText[imageKey]?.let { + drawTextCache[imageKey]?.let { + textBitmap = it + } ?: kotlin.run { + it.paint.isAntiAlias = true + var layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + var lineMax = try { + val field = StaticLayout::class.java.getDeclaredField("mMaximumVisibleLineCount") + field.isAccessible = true + field.getInt(it) + } catch (e: Exception) { + Int.MAX_VALUE + } + StaticLayout.Builder + .obtain(it.text, 0, it.text.length, it.paint, drawingBitmap.width) + .setAlignment(it.alignment) + .setMaxLines(lineMax) + .setEllipsize(TextUtils.TruncateAt.END) + .build() + } else { + StaticLayout(it.text, 0, it.text.length, it.paint, drawingBitmap.width, it.alignment, it.spacingMultiplier, it.spacingAdd, false) + } + textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888) + val textCanvas = Canvas(textBitmap) + textCanvas.translate(0f, ((drawingBitmap.height - layout.height) / 2).toFloat()) + layout.draw(textCanvas) + drawTextCache.put(imageKey, textBitmap as Bitmap) + } + } + textBitmap?.let { textBitmap -> + val paint = this.sharedValues.sharedPaint() + paint.isAntiAlias = videoItem.antiAlias + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + if (sprite.frameEntity.maskPath != null) { + val maskPath = sprite.frameEntity.maskPath ?: return@let + canvas.save() + canvas.concat(frameMatrix) + canvas.clipRect(0, 0, drawingBitmap.width, drawingBitmap.height) + val bitmapShader = BitmapShader(textBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) + paint.shader = bitmapShader + val path = this.sharedValues.sharedPath() + maskPath.buildPath(path) + canvas.drawPath(path, paint) + canvas.restore() + } else { + paint.isFilterBitmap = videoItem.antiAlias + canvas.drawBitmap(textBitmap, frameMatrix, paint) + } + } + } + + private fun drawShape(sprite: SVGADrawerSprite, canvas: Canvas) { + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + sprite.frameEntity.shapes.forEach { shape -> + shape.buildPath() + shape.shapePath?.let { + val paint = this.sharedValues.sharedPaint() + paint.reset() + paint.isAntiAlias = videoItem.antiAlias + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + val path = this.sharedValues.sharedPath() + path.reset() + path.addPath(this.pathCache.buildPath(shape)) + val shapeMatrix = this.sharedValues.sharedMatrix2() + shapeMatrix.reset() + shape.transform?.let { + shapeMatrix.postConcat(it) + } + shapeMatrix.postConcat(frameMatrix) + path.transform(shapeMatrix) + shape.styles?.fill?.let { + if (it != 0x00000000) { + paint.style = Paint.Style.FILL + paint.color = it + val alpha = Math.min(255, Math.max(0, (sprite.frameEntity.alpha * 255).toInt())) + if (alpha != 255) { + paint.alpha = alpha + } + if (sprite.frameEntity.maskPath !== null) canvas.save() + sprite.frameEntity.maskPath?.let { maskPath -> + val path2 = this.sharedValues.sharedPath2() + maskPath.buildPath(path2) + path2.transform(frameMatrix) + canvas.clipPath(path2) + } + canvas.drawPath(path, paint) + if (sprite.frameEntity.maskPath !== null) canvas.restore() + } + } + shape.styles?.strokeWidth?.let { + if (it > 0) { + paint.alpha = (sprite.frameEntity.alpha * 255).toInt() + paint.style = Paint.Style.STROKE + shape.styles?.stroke?.let { + paint.color = it + val alpha = Math.min(255, Math.max(0, (sprite.frameEntity.alpha * 255).toInt())) + if (alpha != 255) { + paint.alpha = alpha + } + } + val scale = matrixScale(frameMatrix) + shape.styles?.strokeWidth?.let { + paint.strokeWidth = it * scale + } + shape.styles?.lineCap?.let { + when { + it.equals("butt", true) -> paint.strokeCap = Paint.Cap.BUTT + it.equals("round", true) -> paint.strokeCap = Paint.Cap.ROUND + it.equals("square", true) -> paint.strokeCap = Paint.Cap.SQUARE + } + } + shape.styles?.lineJoin?.let { + when { + it.equals("miter", true) -> paint.strokeJoin = Paint.Join.MITER + it.equals("round", true) -> paint.strokeJoin = Paint.Join.ROUND + it.equals("bevel", true) -> paint.strokeJoin = Paint.Join.BEVEL + } + } + shape.styles?.miterLimit?.let { + paint.strokeMiter = it.toFloat() * scale + } + shape.styles?.lineDash?.let { + if (it.size == 3 && (it[0] > 0 || it[1] > 0)) { + paint.pathEffect = DashPathEffect(floatArrayOf( + (if (it[0] < 1.0f) 1.0f else it[0]) * scale, + (if (it[1] < 0.1f) 0.1f else it[1]) * scale + ), it[2] * scale) + } + } + if (sprite.frameEntity.maskPath !== null) canvas.save() + sprite.frameEntity.maskPath?.let { maskPath -> + val path2 = this.sharedValues.sharedPath2() + maskPath.buildPath(path2) + path2.transform(frameMatrix) + canvas.clipPath(path2) + } + canvas.drawPath(path, paint) + if (sprite.frameEntity.maskPath !== null) canvas.restore() + } + } + } + + } + } + + private val matrixScaleTempValues = FloatArray(16) + + private fun matrixScale(matrix: Matrix): Float { + matrix.getValues(matrixScaleTempValues) + if (matrixScaleTempValues[0] == 0f) { + return 0f + } + var A = matrixScaleTempValues[0].toDouble() + var B = matrixScaleTempValues[3].toDouble() + var C = matrixScaleTempValues[1].toDouble() + var D = matrixScaleTempValues[4].toDouble() + if (A * D == B * C) return 0f + var scaleX = Math.sqrt(A * A + B * B) + A /= scaleX + B /= scaleX + var skew = A * C + B * D + C -= A * skew + D -= B * skew + var scaleY = Math.sqrt(C * C + D * D) + C /= scaleY + D /= scaleY + skew /= scaleY + if (A * D < B * C) { + scaleX = -scaleX + } + return if (scaleInfo.ratioX) Math.abs(scaleX.toFloat()) else Math.abs(scaleY.toFloat()) + } + + private fun drawDynamic(sprite: SVGADrawerSprite, canvas: Canvas, frameIndex: Int) { + val imageKey = sprite.imageKey ?: return + dynamicItem.dynamicDrawer[imageKey]?.let { + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + canvas.save() + canvas.concat(frameMatrix) + it.invoke(canvas, frameIndex) + canvas.restore() + } + dynamicItem.dynamicDrawerSized[imageKey]?.let { + val frameMatrix = shareFrameMatrix(sprite.frameEntity.transform) + canvas.save() + canvas.concat(frameMatrix) + it.invoke(canvas, frameIndex, sprite.frameEntity.layout.width.toInt(), sprite.frameEntity.layout.height.toInt()) + canvas.restore() + } + } + + class ShareValues { + + private val sharedPaint = Paint() + private val sharedPath = Path() + private val sharedPath2 = Path() + private val sharedMatrix = Matrix() + private val sharedMatrix2 = Matrix() + + private val shareMattePaint = Paint() + private var shareMatteCanvas: Canvas? = null + private var sharedMatteBitmap: Bitmap? = null + + fun sharedPaint(): Paint { + sharedPaint.reset() + return sharedPaint + } + + fun sharedPath(): Path { + sharedPath.reset() + return sharedPath + } + + fun sharedPath2(): Path { + sharedPath2.reset() + return sharedPath2 + } + + fun sharedMatrix(): Matrix { + sharedMatrix.reset() + return sharedMatrix + } + + fun sharedMatrix2(): Matrix { + sharedMatrix2.reset() + return sharedMatrix2 + } + + fun shareMattePaint(): Paint { + shareMattePaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.DST_IN)) + return shareMattePaint + } + + fun sharedMatteBitmap(): Bitmap { + return sharedMatteBitmap as Bitmap + } + + fun shareMatteCanvas(width: Int, height: Int): Canvas { + if (shareMatteCanvas == null) { + sharedMatteBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8) +// shareMatteCanvas = Canvas(sharedMatteBitmap) + } +// val matteCanvas = shareMatteCanvas as Canvas +// matteCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) +// return matteCanvas + return Canvas(sharedMatteBitmap) + } + } + + class PathCache { + + private var canvasWidth: Int = 0 + private var canvasHeight: Int = 0 + private val cache = HashMap() + + fun onSizeChanged(canvas: Canvas) { + if (this.canvasWidth != canvas.width || this.canvasHeight != canvas.height) { + this.cache.clear() + } + this.canvasWidth = canvas.width + this.canvasHeight = canvas.height + } + + fun buildPath(shape: SVGAVideoShapeEntity): Path { + if (!this.cache.containsKey(shape)) { + val path = Path() + path.set(shape.shapePath) + this.cache[shape] = path + } + return this.cache[shape]!! + } + + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt new file mode 100644 index 000000000..4788cc033 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAAudioEntity.kt @@ -0,0 +1,24 @@ +package com.opensource.svgaplayer.entities + +import com.opensource.svgaplayer.proto.AudioEntity +import java.io.FileInputStream + +internal class SVGAAudioEntity { + + val audioKey: String? + val startFrame: Int + val endFrame: Int + val startTime: Int + val totalTime: Int + var soundID: Int? = null + var playID: Int? = null + + constructor(audioItem: AudioEntity) { + this.audioKey = audioItem.audioKey + this.startFrame = audioItem.startFrame ?: 0 + this.endFrame = audioItem.endFrame ?: 0 + this.startTime = audioItem.startTime ?: 0 + this.totalTime = audioItem.totalTime ?: 0 + } + +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt new file mode 100644 index 000000000..d6f582c3c --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAPathEntity.kt @@ -0,0 +1,100 @@ +package com.opensource.svgaplayer.entities + +import android.graphics.Path +import com.opensource.svgaplayer.utils.SVGAPoint +import java.util.* + +private val VALID_METHODS: Set = setOf("M", "L", "H", "V", "C", "S", "Q", "R", "A", "Z", "m", "l", "h", "v", "c", "s", "q", "r", "a", "z") + +class SVGAPathEntity(originValue: String) { + + private val replacedValue: String = if (originValue.contains(",")) originValue.replace(",", " ") else originValue + + private var cachedPath: Path? = null + + fun buildPath(toPath: Path) { + cachedPath?.let { + toPath.set(it) + return + } + val cachedPath = Path() + val segments = StringTokenizer(this.replacedValue, "MLHVCSQRAZmlhvcsqraz", true) + var currentMethod = "" + while (segments.hasMoreTokens()) { + val segment = segments.nextToken() + if (segment.isEmpty()) { continue } + if (VALID_METHODS.contains(segment)) { + currentMethod = segment + if (currentMethod == "Z" || currentMethod == "z") { operate(cachedPath, currentMethod, StringTokenizer("", "")) } + } + else { + operate(cachedPath, currentMethod, StringTokenizer(segment, " ")) + } + } + this.cachedPath = cachedPath + toPath.set(cachedPath) + } + + private fun operate(finalPath: Path, method: String, args: StringTokenizer) { + var x0 = 0.0f + var y0 = 0.0f + var x1 = 0.0f + var y1 = 0.0f + var x2 = 0.0f + var y2 = 0.0f + try { + var index = 0 + while (args.hasMoreTokens()) { + val s = args.nextToken() + if (s.isEmpty()) {continue} + if (index == 0) { x0 = s.toFloat() } + if (index == 1) { y0 = s.toFloat() } + if (index == 2) { x1 = s.toFloat() } + if (index == 3) { y1 = s.toFloat() } + if (index == 4) { x2 = s.toFloat() } + if (index == 5) { y2 = s.toFloat() } + index++ + } + } catch (e: Exception) {} + var currentPoint = SVGAPoint(0.0f, 0.0f, 0.0f) + if (method == "M") { + finalPath.moveTo(x0, y0) + currentPoint = SVGAPoint(x0, y0, 0.0f) + } else if (method == "m") { + finalPath.rMoveTo(x0, y0) + currentPoint = SVGAPoint(currentPoint.x + x0, currentPoint.y + y0, 0.0f) + } + if (method == "L") { + finalPath.lineTo(x0, y0) + } else if (method == "l") { + finalPath.rLineTo(x0, y0) + } + if (method == "C") { + finalPath.cubicTo(x0, y0, x1, y1, x2, y2) + } else if (method == "c") { + finalPath.rCubicTo(x0, y0, x1, y1, x2, y2) + } + if (method == "Q") { + finalPath.quadTo(x0, y0, x1, y1) + } else if (method == "q") { + finalPath.rQuadTo(x0, y0, x1, y1) + } + if (method == "H") { + finalPath.lineTo(x0, currentPoint.y) + } else if (method == "h") { + finalPath.rLineTo(x0, 0f) + } + if (method == "V") { + finalPath.lineTo(currentPoint.x, x0) + } else if (method == "v") { + finalPath.rLineTo(0f, x0) + } + if (method == "Z") { + finalPath.close() + } + else if (method == "z") { + finalPath.close() + } + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt new file mode 100644 index 000000000..1f4cbb94d --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoShapeEntity.kt @@ -0,0 +1,356 @@ +package com.opensource.svgaplayer.entities + +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Path +import android.graphics.RectF +import com.opensource.svgaplayer.proto.ShapeEntity +import org.json.JSONArray +import org.json.JSONObject +import java.util.* + +/** + * Created by cuiminghui on 2017/2/22. + */ + +val sharedPath = Path() + +internal class SVGAVideoShapeEntity { + + enum class Type { + shape, + rect, + ellipse, + keep + } + + class Styles { + + var fill = 0x00000000 + internal set + + var stroke = 0x00000000 + internal set + + var strokeWidth = 0.0f + internal set + + var lineCap = "butt" + internal set + + var lineJoin = "miter" + internal set + + var miterLimit = 0 + internal set + + var lineDash = FloatArray(0) + internal set + + } + + var type = Type.shape + private set + + var args: Map? = null + private set + + var styles: Styles? = null + private set + + var transform: Matrix? = null + private set + + constructor(obj: JSONObject) { + parseType(obj) + parseArgs(obj) + parseStyles(obj) + parseTransform(obj) + } + + constructor(obj: ShapeEntity) { + parseType(obj) + parseArgs(obj) + parseStyles(obj) + parseTransform(obj) + } + + val isKeep: Boolean + get() = type == Type.keep + + var shapePath: Path? = null + + private fun parseType(obj: JSONObject) { + obj.optString("type")?.let { + when { + it.equals("shape", ignoreCase = true) -> type = Type.shape + it.equals("rect", ignoreCase = true) -> type = Type.rect + it.equals("ellipse", ignoreCase = true) -> type = Type.ellipse + it.equals("keep", ignoreCase = true) -> type = Type.keep + } + } + } + + private fun parseType(obj: ShapeEntity) { + obj.type?.let { + type = when (it) { + ShapeEntity.ShapeType.SHAPE -> Type.shape + ShapeEntity.ShapeType.RECT -> Type.rect + ShapeEntity.ShapeType.ELLIPSE -> Type.ellipse + ShapeEntity.ShapeType.KEEP -> Type.keep + } + } + } + + private fun parseArgs(obj: JSONObject) { + val args = HashMap() + obj.optJSONObject("args")?.let { values -> + values.keys().forEach { key -> + values.get(key)?.let { + args.put(key, it) + } + } + this.args = args + } + } + + private fun parseArgs(obj: ShapeEntity) { + val args = HashMap() + obj.shape?.let { + it.d?.let { args.put("d", it) } + } + obj.ellipse?.let { + args.put("x", it.x ?: 0.0f) + args.put("y", it.y ?: 0.0f) + args.put("radiusX", it.radiusX ?: 0.0f) + args.put("radiusY", it.radiusY ?: 0.0f) + } + obj.rect?.let { + args.put("x", it.x ?: 0.0f) + args.put("y", it.y ?: 0.0f) + args.put("width", it.width ?: 0.0f) + args.put("height", it.height ?: 0.0f) + args.put("cornerRadius", it.cornerRadius ?: 0.0f) + } + this.args = args + } + + // 检查色域范围是否是 [0f, 1f],或者是 [0f, 255f] + private fun checkValueRange(obj: JSONArray): Float { + return if ( + obj.optDouble(0) <= 1 && + obj.optDouble(1) <= 1 && + obj.optDouble(2) <= 1 + ) { + 255f + } else { + 1f + } + } + + // 检查 alpha 的范围是否是 [0f, 1f],或者是 [0f, 255f] + private fun checkAlphaValueRange(obj: JSONArray): Float { + return if (obj.optDouble(3) <= 1) { + 255f + } else { + 1f + } + } + + private fun parseStyles(obj: JSONObject) { + obj.optJSONObject("styles")?.let { + val styles = Styles() + it.optJSONArray("fill")?.let { + if (it.length() == 4) { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.fill = Color.argb( + (it.optDouble(3) * alphaRangeValue).toInt(), + (it.optDouble(0) * mulValue).toInt(), + (it.optDouble(1) * mulValue).toInt(), + (it.optDouble(2) * mulValue).toInt() + ) + } + } + it.optJSONArray("stroke")?.let { + if (it.length() == 4) { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.stroke = Color.argb( + (it.optDouble(3) * alphaRangeValue).toInt(), + (it.optDouble(0) * mulValue).toInt(), + (it.optDouble(1) * mulValue).toInt(), + (it.optDouble(2) * mulValue).toInt() + ) + } + } + styles.strokeWidth = it.optDouble("strokeWidth", 0.0).toFloat() + styles.lineCap = it.optString("lineCap", "butt") + styles.lineJoin = it.optString("lineJoin", "miter") + styles.miterLimit = it.optInt("miterLimit", 0) + it.optJSONArray("lineDash")?.let { + styles.lineDash = FloatArray(it.length()) + for (i in 0 until it.length()) { + styles.lineDash[i] = it.optDouble(i, 0.0).toFloat() + } + } + this.styles = styles + } + } + + // 检查色域范围是否是 [0f, 1f],或者是 [0f, 255f] + private fun checkValueRange(color: ShapeEntity.ShapeStyle.RGBAColor): Float { + return if ( + (color.r ?: 0f) <= 1 && + (color.g ?: 0f) <= 1 && + (color.b ?: 0f) <= 1 + ) { + 255f + } else { + 1f + } + } + + // 检查 alpha 范围是否是 [0f, 1f],有可能是 [0f, 255f] + private fun checkAlphaValueRange(color: ShapeEntity.ShapeStyle.RGBAColor): Float { + return if (color.a <= 1f) { + 255f + } else { + 1f + } + } + + private fun parseStyles(obj: ShapeEntity) { + obj.styles?.let { + val styles = Styles() + it.fill?.let { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.fill = Color.argb( + ((it.a ?: 0f) * alphaRangeValue).toInt(), + ((it.r ?: 0f) * mulValue).toInt(), + ((it.g ?: 0f) * mulValue).toInt(), + ((it.b ?: 0f) * mulValue).toInt() + ) + } + it.stroke?.let { + val mulValue = checkValueRange(it) + val alphaRangeValue = checkAlphaValueRange(it) + styles.stroke = Color.argb( + ((it.a ?: 0f) * alphaRangeValue).toInt(), + ((it.r ?: 0f) * mulValue).toInt(), + ((it.g ?: 0f) * mulValue).toInt(), + ((it.b ?: 0f) * mulValue).toInt() + ) + + } + styles.strokeWidth = it.strokeWidth ?: 0.0f + it.lineCap?.let { + when (it) { + ShapeEntity.ShapeStyle.LineCap.LineCap_BUTT -> styles.lineCap = "butt" + ShapeEntity.ShapeStyle.LineCap.LineCap_ROUND -> styles.lineCap = "round" + ShapeEntity.ShapeStyle.LineCap.LineCap_SQUARE -> styles.lineCap = "square" + } + } + it.lineJoin?.let { + when (it) { + ShapeEntity.ShapeStyle.LineJoin.LineJoin_BEVEL -> styles.lineJoin = "bevel" + ShapeEntity.ShapeStyle.LineJoin.LineJoin_MITER -> styles.lineJoin = "miter" + ShapeEntity.ShapeStyle.LineJoin.LineJoin_ROUND -> styles.lineJoin = "round" + } + } + styles.miterLimit = (it.miterLimit ?: 0.0f).toInt() + styles.lineDash = kotlin.FloatArray(3) + it.lineDashI?.let { styles.lineDash[0] = it } + it.lineDashII?.let { styles.lineDash[1] = it } + it.lineDashIII?.let { styles.lineDash[2] = it } + this.styles = styles + } + } + + private fun parseTransform(obj: JSONObject) { + obj.optJSONObject("transform")?.let { + val transform = Matrix() + val arr = FloatArray(9) + val a = it.optDouble("a", 1.0) + val b = it.optDouble("b", 0.0) + val c = it.optDouble("c", 0.0) + val d = it.optDouble("d", 1.0) + val tx = it.optDouble("tx", 0.0) + val ty = it.optDouble("ty", 0.0) + arr[0] = a.toFloat() // a + arr[1] = c.toFloat() // c + arr[2] = tx.toFloat() // tx + arr[3] = b.toFloat() // b + arr[4] = d.toFloat() // d + arr[5] = ty.toFloat() // ty + arr[6] = 0.0.toFloat() + arr[7] = 0.0.toFloat() + arr[8] = 1.0.toFloat() + transform.setValues(arr) + this.transform = transform + } + } + + private fun parseTransform(obj: ShapeEntity) { + obj.transform?.let { + val transform = Matrix() + val arr = FloatArray(9) + val a = it.a ?: 1.0f + val b = it.b ?: 0.0f + val c = it.c ?: 0.0f + val d = it.d ?: 1.0f + val tx = it.tx ?: 0.0f + val ty = it.ty ?: 0.0f + arr[0] = a + arr[1] = c + arr[2] = tx + arr[3] = b + arr[4] = d + arr[5] = ty + arr[6] = 0.0f + arr[7] = 0.0f + arr[8] = 1.0f + transform.setValues(arr) + this.transform = transform + } + } + + + fun buildPath() { + if (this.shapePath != null) { + return + } + sharedPath.reset() + if (this.type == Type.shape) { + (this.args?.get("d") as? String)?.let { + SVGAPathEntity(it).buildPath(sharedPath) + } + } else if (this.type == Type.ellipse) { + val xv = this.args?.get("x") as? Number ?: return + val yv = this.args?.get("y") as? Number ?: return + val rxv = this.args?.get("radiusX") as? Number ?: return + val ryv = this.args?.get("radiusY") as? Number ?: return + val x = xv.toFloat() + val y = yv.toFloat() + val rx = rxv.toFloat() + val ry = ryv.toFloat() + sharedPath.addOval(RectF(x - rx, y - ry, x + rx, y + ry), Path.Direction.CW) + } else if (this.type == Type.rect) { + val xv = this.args?.get("x") as? Number ?: return + val yv = this.args?.get("y") as? Number ?: return + val wv = this.args?.get("width") as? Number ?: return + val hv = this.args?.get("height") as? Number ?: return + val crv = this.args?.get("cornerRadius") as? Number ?: return + val x = xv.toFloat() + val y = yv.toFloat() + val width = wv.toFloat() + val height = hv.toFloat() + val cornerRadius = crv.toFloat() + sharedPath.addRoundRect(RectF(x, y, x + width, y + height), cornerRadius, cornerRadius, Path.Direction.CW) + } + this.shapePath = Path() + this.shapePath?.set(sharedPath) + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt new file mode 100644 index 000000000..6e9fbc28e --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteEntity.kt @@ -0,0 +1,60 @@ +package com.opensource.svgaplayer.entities + +import com.opensource.svgaplayer.proto.SpriteEntity +import org.json.JSONObject + +/** + * Created by cuiminghui on 2016/10/17. + */ +internal class SVGAVideoSpriteEntity { + + val imageKey: String? + + val matteKey: String? + + val frames: List + + constructor(obj: JSONObject) { + this.imageKey = obj.optString("imageKey") + this.matteKey = obj.optString("matteKey") + val mutableFrames: MutableList = mutableListOf() + obj.optJSONArray("frames")?.let { + for (i in 0 until it.length()) { + it.optJSONObject(i)?.let { + val frameItem = SVGAVideoSpriteFrameEntity(it) + if (frameItem.shapes.isNotEmpty()) { + frameItem.shapes.first().let { + if (it.isKeep && mutableFrames.size > 0) { + frameItem.shapes = mutableFrames.last().shapes + } + } + } + mutableFrames.add(frameItem) + } + } + } + frames = mutableFrames.toList() + } + + constructor(obj: SpriteEntity) { + this.imageKey = obj.imageKey + this.matteKey = obj.matteKey + var lastFrame: SVGAVideoSpriteFrameEntity? = null + frames = obj.frames?.map { + val frameItem = SVGAVideoSpriteFrameEntity(it) + if (frameItem.shapes.isNotEmpty()) { + frameItem.shapes.first().let { + if (it.isKeep) { + lastFrame?.let { + frameItem.shapes = it.shapes + } + } + } + } + lastFrame = frameItem + return@map frameItem + } ?: listOf() + + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt new file mode 100644 index 000000000..078d91a36 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/entities/SVGAVideoSpriteFrameEntity.kt @@ -0,0 +1,94 @@ +package com.opensource.svgaplayer.entities + +import android.graphics.Matrix +import com.opensource.svgaplayer.proto.FrameEntity +import com.opensource.svgaplayer.utils.SVGARect + +import org.json.JSONObject + +/** + * Created by cuiminghui on 2016/10/17. + */ +internal class SVGAVideoSpriteFrameEntity { + + var alpha: Double + var layout = SVGARect(0.0, 0.0, 0.0, 0.0) + var transform = Matrix() + var maskPath: SVGAPathEntity? = null + var shapes: List = listOf() + + constructor(obj: JSONObject) { + this.alpha = obj.optDouble("alpha", 0.0) + obj.optJSONObject("layout")?.let { + layout = SVGARect(it.optDouble("x", 0.0), it.optDouble("y", 0.0), it.optDouble("width", 0.0), it.optDouble("height", 0.0)) + } + obj.optJSONObject("transform")?.let { + val arr = FloatArray(9) + val a = it.optDouble("a", 1.0) + val b = it.optDouble("b", 0.0) + val c = it.optDouble("c", 0.0) + val d = it.optDouble("d", 1.0) + val tx = it.optDouble("tx", 0.0) + val ty = it.optDouble("ty", 0.0) + arr[0] = a.toFloat() + arr[1] = c.toFloat() + arr[2] = tx.toFloat() + arr[3] = b.toFloat() + arr[4] = d.toFloat() + arr[5] = ty.toFloat() + arr[6] = 0.0.toFloat() + arr[7] = 0.0.toFloat() + arr[8] = 1.0.toFloat() + transform.setValues(arr) + } + obj.optString("clipPath")?.let { d -> + if (d.isNotEmpty()) { + maskPath = SVGAPathEntity(d) + } + } + obj.optJSONArray("shapes")?.let { + val mutableList: MutableList = mutableListOf() + for (i in 0 until it.length()) { + it.optJSONObject(i)?.let { + mutableList.add(SVGAVideoShapeEntity(it)) + } + } + shapes = mutableList.toList() + } + } + + constructor(obj: FrameEntity) { + this.alpha = (obj.alpha ?: 0.0f).toDouble() + obj.layout?.let { + this.layout = SVGARect((it.x ?: 0.0f).toDouble(), (it.y + ?: 0.0f).toDouble(), (it.width ?: 0.0f).toDouble(), (it.height + ?: 0.0f).toDouble()) + } + obj.transform?.let { + val arr = FloatArray(9) + val a = it.a ?: 1.0f + val b = it.b ?: 0.0f + val c = it.c ?: 0.0f + val d = it.d ?: 1.0f + val tx = it.tx ?: 0.0f + val ty = it.ty ?: 0.0f + arr[0] = a + arr[1] = c + arr[2] = tx + arr[3] = b + arr[4] = d + arr[5] = ty + arr[6] = 0.0f + arr[7] = 0.0f + arr[8] = 1.0f + transform.setValues(arr) + } + obj.clipPath?.takeIf { it.isNotEmpty() }?.let { + maskPath = SVGAPathEntity(it) + } + this.shapes = obj.shapes.map { + return@map SVGAVideoShapeEntity(it) + } + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java new file mode 100644 index 000000000..d09adce90 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/AudioEntity.java @@ -0,0 +1,258 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 19:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Integer; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import okio.ByteString; + +public final class AudioEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_AudioEntity(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_AUDIOKEY = ""; + + public static final Integer DEFAULT_STARTFRAME = 0; + + public static final Integer DEFAULT_ENDFRAME = 0; + + public static final Integer DEFAULT_STARTTIME = 0; + + public static final Integer DEFAULT_TOTALTIME = 0; + + /** + * 音频文件名 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String audioKey; + + /** + * 音频播放起始帧 + */ + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer startFrame; + + /** + * 音频播放结束帧 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer endFrame; + + /** + * 音频播放起始时间(相对音频长度) + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer startTime; + + /** + * 音频总长度 + */ + @WireField( + tag = 5, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer totalTime; + + public AudioEntity(String audioKey, Integer startFrame, Integer endFrame, Integer startTime, Integer totalTime) { + this(audioKey, startFrame, endFrame, startTime, totalTime, ByteString.EMPTY); + } + + public AudioEntity(String audioKey, Integer startFrame, Integer endFrame, Integer startTime, Integer totalTime, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.audioKey = audioKey; + this.startFrame = startFrame; + this.endFrame = endFrame; + this.startTime = startTime; + this.totalTime = totalTime; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.audioKey = audioKey; + builder.startFrame = startFrame; + builder.endFrame = endFrame; + builder.startTime = startTime; + builder.totalTime = totalTime; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof AudioEntity)) return false; + AudioEntity o = (AudioEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(audioKey, o.audioKey) + && Internal.equals(startFrame, o.startFrame) + && Internal.equals(endFrame, o.endFrame) + && Internal.equals(startTime, o.startTime) + && Internal.equals(totalTime, o.totalTime); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (audioKey != null ? audioKey.hashCode() : 0); + result = result * 37 + (startFrame != null ? startFrame.hashCode() : 0); + result = result * 37 + (endFrame != null ? endFrame.hashCode() : 0); + result = result * 37 + (startTime != null ? startTime.hashCode() : 0); + result = result * 37 + (totalTime != null ? totalTime.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (audioKey != null) builder.append(", audioKey=").append(audioKey); + if (startFrame != null) builder.append(", startFrame=").append(startFrame); + if (endFrame != null) builder.append(", endFrame=").append(endFrame); + if (startTime != null) builder.append(", startTime=").append(startTime); + if (totalTime != null) builder.append(", totalTime=").append(totalTime); + return builder.replace(0, 2, "AudioEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String audioKey; + + public Integer startFrame; + + public Integer endFrame; + + public Integer startTime; + + public Integer totalTime; + + public Builder() { + } + + /** + * 音频文件名 + */ + public Builder audioKey(String audioKey) { + this.audioKey = audioKey; + return this; + } + + /** + * 音频播放起始帧 + */ + public Builder startFrame(Integer startFrame) { + this.startFrame = startFrame; + return this; + } + + /** + * 音频播放结束帧 + */ + public Builder endFrame(Integer endFrame) { + this.endFrame = endFrame; + return this; + } + + /** + * 音频播放起始时间(相对音频长度) + */ + public Builder startTime(Integer startTime) { + this.startTime = startTime; + return this; + } + + /** + * 音频总长度 + */ + public Builder totalTime(Integer totalTime) { + this.totalTime = totalTime; + return this; + } + + @Override + public AudioEntity build() { + return new AudioEntity(audioKey, startFrame, endFrame, startTime, totalTime, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_AudioEntity extends ProtoAdapter { + ProtoAdapter_AudioEntity() { + super(FieldEncoding.LENGTH_DELIMITED, AudioEntity.class); + } + + @Override + public int encodedSize(AudioEntity value) { + return (value.audioKey != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.audioKey) : 0) + + (value.startFrame != null ? ProtoAdapter.INT32.encodedSizeWithTag(2, value.startFrame) : 0) + + (value.endFrame != null ? ProtoAdapter.INT32.encodedSizeWithTag(3, value.endFrame) : 0) + + (value.startTime != null ? ProtoAdapter.INT32.encodedSizeWithTag(4, value.startTime) : 0) + + (value.totalTime != null ? ProtoAdapter.INT32.encodedSizeWithTag(5, value.totalTime) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, AudioEntity value) throws IOException { + if (value.audioKey != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.audioKey); + if (value.startFrame != null) ProtoAdapter.INT32.encodeWithTag(writer, 2, value.startFrame); + if (value.endFrame != null) ProtoAdapter.INT32.encodeWithTag(writer, 3, value.endFrame); + if (value.startTime != null) ProtoAdapter.INT32.encodeWithTag(writer, 4, value.startTime); + if (value.totalTime != null) ProtoAdapter.INT32.encodeWithTag(writer, 5, value.totalTime); + writer.writeBytes(value.unknownFields()); + } + + @Override + public AudioEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.audioKey(ProtoAdapter.STRING.decode(reader)); break; + case 2: builder.startFrame(ProtoAdapter.INT32.decode(reader)); break; + case 3: builder.endFrame(ProtoAdapter.INT32.decode(reader)); break; + case 4: builder.startTime(ProtoAdapter.INT32.decode(reader)); break; + case 5: builder.totalTime(ProtoAdapter.INT32.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public AudioEntity redact(AudioEntity value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java new file mode 100644 index 000000000..ac701bb43 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/FrameEntity.java @@ -0,0 +1,259 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 115:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Float; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import okio.ByteString; + +public final class FrameEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_FrameEntity(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_ALPHA = 0.0f; + + public static final String DEFAULT_CLIPPATH = ""; + + /** + * 透明度 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float alpha; + + /** + * 初始约束大小 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.Layout#ADAPTER" + ) + public final Layout layout; + + /** + * 2D 变换矩阵 + */ + @WireField( + tag = 3, + adapter = "com.opensource.svgaplayer.proto.Transform#ADAPTER" + ) + public final Transform transform; + + /** + * 遮罩路径,使用 SVG 标准 Path 绘制图案进行 Mask 遮罩。 + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String clipPath; + + /** + * 矢量元素列表 + */ + @WireField( + tag = 5, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List shapes; + + public FrameEntity(Float alpha, Layout layout, Transform transform, String clipPath, List shapes) { + this(alpha, layout, transform, clipPath, shapes, ByteString.EMPTY); + } + + public FrameEntity(Float alpha, Layout layout, Transform transform, String clipPath, List shapes, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.alpha = alpha; + this.layout = layout; + this.transform = transform; + this.clipPath = clipPath; + this.shapes = Internal.immutableCopyOf("shapes", shapes); + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.alpha = alpha; + builder.layout = layout; + builder.transform = transform; + builder.clipPath = clipPath; + builder.shapes = Internal.copyOf("shapes", shapes); + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof FrameEntity)) return false; + FrameEntity o = (FrameEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(alpha, o.alpha) + && Internal.equals(layout, o.layout) + && Internal.equals(transform, o.transform) + && Internal.equals(clipPath, o.clipPath) + && shapes.equals(o.shapes); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (alpha != null ? alpha.hashCode() : 0); + result = result * 37 + (layout != null ? layout.hashCode() : 0); + result = result * 37 + (transform != null ? transform.hashCode() : 0); + result = result * 37 + (clipPath != null ? clipPath.hashCode() : 0); + result = result * 37 + shapes.hashCode(); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (alpha != null) builder.append(", alpha=").append(alpha); + if (layout != null) builder.append(", layout=").append(layout); + if (transform != null) builder.append(", transform=").append(transform); + if (clipPath != null) builder.append(", clipPath=").append(clipPath); + if (!shapes.isEmpty()) builder.append(", shapes=").append(shapes); + return builder.replace(0, 2, "FrameEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float alpha; + + public Layout layout; + + public Transform transform; + + public String clipPath; + + public List shapes; + + public Builder() { + shapes = Internal.newMutableList(); + } + + /** + * 透明度 + */ + public Builder alpha(Float alpha) { + this.alpha = alpha; + return this; + } + + /** + * 初始约束大小 + */ + public Builder layout(Layout layout) { + this.layout = layout; + return this; + } + + /** + * 2D 变换矩阵 + */ + public Builder transform(Transform transform) { + this.transform = transform; + return this; + } + + /** + * 遮罩路径,使用 SVG 标准 Path 绘制图案进行 Mask 遮罩。 + */ + public Builder clipPath(String clipPath) { + this.clipPath = clipPath; + return this; + } + + /** + * 矢量元素列表 + */ + public Builder shapes(List shapes) { + Internal.checkElementsNotNull(shapes); + this.shapes = shapes; + return this; + } + + @Override + public FrameEntity build() { + return new FrameEntity(alpha, layout, transform, clipPath, shapes, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_FrameEntity extends ProtoAdapter { + ProtoAdapter_FrameEntity() { + super(FieldEncoding.LENGTH_DELIMITED, FrameEntity.class); + } + + @Override + public int encodedSize(FrameEntity value) { + return (value.alpha != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.alpha) : 0) + + (value.layout != null ? Layout.ADAPTER.encodedSizeWithTag(2, value.layout) : 0) + + (value.transform != null ? Transform.ADAPTER.encodedSizeWithTag(3, value.transform) : 0) + + (value.clipPath != null ? ProtoAdapter.STRING.encodedSizeWithTag(4, value.clipPath) : 0) + + ShapeEntity.ADAPTER.asRepeated().encodedSizeWithTag(5, value.shapes) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, FrameEntity value) throws IOException { + if (value.alpha != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.alpha); + if (value.layout != null) Layout.ADAPTER.encodeWithTag(writer, 2, value.layout); + if (value.transform != null) Transform.ADAPTER.encodeWithTag(writer, 3, value.transform); + if (value.clipPath != null) ProtoAdapter.STRING.encodeWithTag(writer, 4, value.clipPath); + ShapeEntity.ADAPTER.asRepeated().encodeWithTag(writer, 5, value.shapes); + writer.writeBytes(value.unknownFields()); + } + + @Override + public FrameEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.alpha(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.layout(Layout.ADAPTER.decode(reader)); break; + case 3: builder.transform(Transform.ADAPTER.decode(reader)); break; + case 4: builder.clipPath(ProtoAdapter.STRING.decode(reader)); break; + case 5: builder.shapes.add(ShapeEntity.ADAPTER.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public FrameEntity redact(FrameEntity value) { + Builder builder = value.newBuilder(); + if (builder.layout != null) builder.layout = Layout.ADAPTER.redact(builder.layout); + if (builder.transform != null) builder.transform = Transform.ADAPTER.redact(builder.transform); + Internal.redactElements(builder.shapes, ShapeEntity.ADAPTER); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/Layout.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/Layout.java new file mode 100644 index 000000000..6ea7edb6c --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/Layout.java @@ -0,0 +1,205 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 27:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Float; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import okio.ByteString; + +public final class Layout extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_Layout(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_X = 0.0f; + + public static final Float DEFAULT_Y = 0.0f; + + public static final Float DEFAULT_WIDTH = 0.0f; + + public static final Float DEFAULT_HEIGHT = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float x; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float y; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float width; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float height; + + public Layout(Float x, Float y, Float width, Float height) { + this(x, y, width, height, ByteString.EMPTY); + } + + public Layout(Float x, Float y, Float width, Float height, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.x = x; + builder.y = y; + builder.width = width; + builder.height = height; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof Layout)) return false; + Layout o = (Layout) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(x, o.x) + && Internal.equals(y, o.y) + && Internal.equals(width, o.width) + && Internal.equals(height, o.height); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (x != null ? x.hashCode() : 0); + result = result * 37 + (y != null ? y.hashCode() : 0); + result = result * 37 + (width != null ? width.hashCode() : 0); + result = result * 37 + (height != null ? height.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (x != null) builder.append(", x=").append(x); + if (y != null) builder.append(", y=").append(y); + if (width != null) builder.append(", width=").append(width); + if (height != null) builder.append(", height=").append(height); + return builder.replace(0, 2, "Layout{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float x; + + public Float y; + + public Float width; + + public Float height; + + public Builder() { + } + + public Builder x(Float x) { + this.x = x; + return this; + } + + public Builder y(Float y) { + this.y = y; + return this; + } + + public Builder width(Float width) { + this.width = width; + return this; + } + + public Builder height(Float height) { + this.height = height; + return this; + } + + @Override + public Layout build() { + return new Layout(x, y, width, height, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_Layout extends ProtoAdapter { + ProtoAdapter_Layout() { + super(FieldEncoding.LENGTH_DELIMITED, Layout.class); + } + + @Override + public int encodedSize(Layout value) { + return (value.x != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.x) : 0) + + (value.y != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.y) : 0) + + (value.width != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.width) : 0) + + (value.height != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.height) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, Layout value) throws IOException { + if (value.x != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.x); + if (value.y != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.y); + if (value.width != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.width); + if (value.height != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.height); + writer.writeBytes(value.unknownFields()); + } + + @Override + public Layout decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.x(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.y(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.width(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.height(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public Layout redact(Layout value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java new file mode 100644 index 000000000..6b720af45 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/MovieEntity.java @@ -0,0 +1,265 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 123:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import java.util.Map; +import okio.ByteString; + +public final class MovieEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_MovieEntity(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_VERSION = ""; + + /** + * SVGA 格式版本号 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String version; + + /** + * 动画参数 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.MovieParams#ADAPTER" + ) + public final MovieParams params; + + /** + * Key 是位图键名,Value 是位图文件名或二进制 PNG 数据。 + */ + @WireField( + tag = 3, + keyAdapter = "com.squareup.wire.ProtoAdapter#STRING", + adapter = "com.squareup.wire.ProtoAdapter#BYTES" + ) + public final Map images; + + /** + * 元素列表 + */ + @WireField( + tag = 4, + adapter = "com.opensource.svgaplayer.proto.SpriteEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List sprites; + + /** + * 音频列表 + */ + @WireField( + tag = 5, + adapter = "com.opensource.svgaplayer.proto.AudioEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List audios; + + public MovieEntity(String version, MovieParams params, Map images, List sprites, List audios) { + this(version, params, images, sprites, audios, ByteString.EMPTY); + } + + public MovieEntity(String version, MovieParams params, Map images, List sprites, List audios, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.version = version; + this.params = params; + this.images = Internal.immutableCopyOf("images", images); + this.sprites = Internal.immutableCopyOf("sprites", sprites); + this.audios = Internal.immutableCopyOf("audios", audios); + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.version = version; + builder.params = params; + builder.images = Internal.copyOf("images", images); + builder.sprites = Internal.copyOf("sprites", sprites); + builder.audios = Internal.copyOf("audios", audios); + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof MovieEntity)) return false; + MovieEntity o = (MovieEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(version, o.version) + && Internal.equals(params, o.params) + && images.equals(o.images) + && sprites.equals(o.sprites) + && audios.equals(o.audios); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (version != null ? version.hashCode() : 0); + result = result * 37 + (params != null ? params.hashCode() : 0); + result = result * 37 + images.hashCode(); + result = result * 37 + sprites.hashCode(); + result = result * 37 + audios.hashCode(); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (version != null) builder.append(", version=").append(version); + if (params != null) builder.append(", params=").append(params); + if (!images.isEmpty()) builder.append(", images=").append(images); + if (!sprites.isEmpty()) builder.append(", sprites=").append(sprites); + if (!audios.isEmpty()) builder.append(", audios=").append(audios); + return builder.replace(0, 2, "MovieEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String version; + + public MovieParams params; + + public Map images; + + public List sprites; + + public List audios; + + public Builder() { + images = Internal.newMutableMap(); + sprites = Internal.newMutableList(); + audios = Internal.newMutableList(); + } + + /** + * SVGA 格式版本号 + */ + public Builder version(String version) { + this.version = version; + return this; + } + + /** + * 动画参数 + */ + public Builder params(MovieParams params) { + this.params = params; + return this; + } + + /** + * Key 是位图键名,Value 是位图文件名或二进制 PNG 数据。 + */ + public Builder images(Map images) { + Internal.checkElementsNotNull(images); + this.images = images; + return this; + } + + /** + * 元素列表 + */ + public Builder sprites(List sprites) { + Internal.checkElementsNotNull(sprites); + this.sprites = sprites; + return this; + } + + /** + * 音频列表 + */ + public Builder audios(List audios) { + Internal.checkElementsNotNull(audios); + this.audios = audios; + return this; + } + + @Override + public MovieEntity build() { + return new MovieEntity(version, params, images, sprites, audios, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_MovieEntity extends ProtoAdapter { + private final ProtoAdapter> images = ProtoAdapter.newMapAdapter(ProtoAdapter.STRING, ProtoAdapter.BYTES); + + ProtoAdapter_MovieEntity() { + super(FieldEncoding.LENGTH_DELIMITED, MovieEntity.class); + } + + @Override + public int encodedSize(MovieEntity value) { + return (value.version != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.version) : 0) + + (value.params != null ? MovieParams.ADAPTER.encodedSizeWithTag(2, value.params) : 0) + + images.encodedSizeWithTag(3, value.images) + + SpriteEntity.ADAPTER.asRepeated().encodedSizeWithTag(4, value.sprites) + + AudioEntity.ADAPTER.asRepeated().encodedSizeWithTag(5, value.audios) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, MovieEntity value) throws IOException { + if (value.version != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.version); + if (value.params != null) MovieParams.ADAPTER.encodeWithTag(writer, 2, value.params); + images.encodeWithTag(writer, 3, value.images); + SpriteEntity.ADAPTER.asRepeated().encodeWithTag(writer, 4, value.sprites); + AudioEntity.ADAPTER.asRepeated().encodeWithTag(writer, 5, value.audios); + writer.writeBytes(value.unknownFields()); + } + + @Override + public MovieEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.version(ProtoAdapter.STRING.decode(reader)); break; + case 2: builder.params(MovieParams.ADAPTER.decode(reader)); break; + case 3: builder.images.putAll(images.decode(reader)); break; + case 4: builder.sprites.add(SpriteEntity.ADAPTER.decode(reader)); break; + case 5: builder.audios.add(AudioEntity.ADAPTER.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public MovieEntity redact(MovieEntity value) { + Builder builder = value.newBuilder(); + if (builder.params != null) builder.params = MovieParams.ADAPTER.redact(builder.params); + Internal.redactElements(builder.sprites, SpriteEntity.ADAPTER); + Internal.redactElements(builder.audios, AudioEntity.ADAPTER); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java new file mode 100644 index 000000000..e8a3a98b8 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/MovieParams.java @@ -0,0 +1,230 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 6:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Float; +import java.lang.Integer; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import okio.ByteString; + +public final class MovieParams extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_MovieParams(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_VIEWBOXWIDTH = 0.0f; + + public static final Float DEFAULT_VIEWBOXHEIGHT = 0.0f; + + public static final Integer DEFAULT_FPS = 0; + + public static final Integer DEFAULT_FRAMES = 0; + + /** + * 画布宽 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float viewBoxWidth; + + /** + * 画布高 + */ + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float viewBoxHeight; + + /** + * 动画每秒播放帧数,合法值是 [1, 2, 3, 5, 6, 10, 12, 15, 20, 30, 60] 中的任意一个。 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer fps; + + /** + * 动画总帧数 + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#INT32" + ) + public final Integer frames; + + public MovieParams(Float viewBoxWidth, Float viewBoxHeight, Integer fps, Integer frames) { + this(viewBoxWidth, viewBoxHeight, fps, frames, ByteString.EMPTY); + } + + public MovieParams(Float viewBoxWidth, Float viewBoxHeight, Integer fps, Integer frames, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.viewBoxWidth = viewBoxWidth; + this.viewBoxHeight = viewBoxHeight; + this.fps = fps; + this.frames = frames; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.viewBoxWidth = viewBoxWidth; + builder.viewBoxHeight = viewBoxHeight; + builder.fps = fps; + builder.frames = frames; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof MovieParams)) return false; + MovieParams o = (MovieParams) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(viewBoxWidth, o.viewBoxWidth) + && Internal.equals(viewBoxHeight, o.viewBoxHeight) + && Internal.equals(fps, o.fps) + && Internal.equals(frames, o.frames); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (viewBoxWidth != null ? viewBoxWidth.hashCode() : 0); + result = result * 37 + (viewBoxHeight != null ? viewBoxHeight.hashCode() : 0); + result = result * 37 + (fps != null ? fps.hashCode() : 0); + result = result * 37 + (frames != null ? frames.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (viewBoxWidth != null) builder.append(", viewBoxWidth=").append(viewBoxWidth); + if (viewBoxHeight != null) builder.append(", viewBoxHeight=").append(viewBoxHeight); + if (fps != null) builder.append(", fps=").append(fps); + if (frames != null) builder.append(", frames=").append(frames); + return builder.replace(0, 2, "MovieParams{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float viewBoxWidth; + + public Float viewBoxHeight; + + public Integer fps; + + public Integer frames; + + public Builder() { + } + + /** + * 画布宽 + */ + public Builder viewBoxWidth(Float viewBoxWidth) { + this.viewBoxWidth = viewBoxWidth; + return this; + } + + /** + * 画布高 + */ + public Builder viewBoxHeight(Float viewBoxHeight) { + this.viewBoxHeight = viewBoxHeight; + return this; + } + + /** + * 动画每秒播放帧数,合法值是 [1, 2, 3, 5, 6, 10, 12, 15, 20, 30, 60] 中的任意一个。 + */ + public Builder fps(Integer fps) { + this.fps = fps; + return this; + } + + /** + * 动画总帧数 + */ + public Builder frames(Integer frames) { + this.frames = frames; + return this; + } + + @Override + public MovieParams build() { + return new MovieParams(viewBoxWidth, viewBoxHeight, fps, frames, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_MovieParams extends ProtoAdapter { + ProtoAdapter_MovieParams() { + super(FieldEncoding.LENGTH_DELIMITED, MovieParams.class); + } + + @Override + public int encodedSize(MovieParams value) { + return (value.viewBoxWidth != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.viewBoxWidth) : 0) + + (value.viewBoxHeight != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.viewBoxHeight) : 0) + + (value.fps != null ? ProtoAdapter.INT32.encodedSizeWithTag(3, value.fps) : 0) + + (value.frames != null ? ProtoAdapter.INT32.encodedSizeWithTag(4, value.frames) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, MovieParams value) throws IOException { + if (value.viewBoxWidth != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.viewBoxWidth); + if (value.viewBoxHeight != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.viewBoxHeight); + if (value.fps != null) ProtoAdapter.INT32.encodeWithTag(writer, 3, value.fps); + if (value.frames != null) ProtoAdapter.INT32.encodeWithTag(writer, 4, value.frames); + writer.writeBytes(value.unknownFields()); + } + + @Override + public MovieParams decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.viewBoxWidth(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.viewBoxHeight(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.fps(ProtoAdapter.INT32.decode(reader)); break; + case 4: builder.frames(ProtoAdapter.INT32.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public MovieParams redact(MovieParams value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java new file mode 100644 index 000000000..9017dfb62 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/ShapeEntity.java @@ -0,0 +1,1503 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 43:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireEnum; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Float; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import okio.ByteString; + +public final class ShapeEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_ShapeEntity(); + + private static final long serialVersionUID = 0L; + + public static final ShapeType DEFAULT_TYPE = ShapeType.SHAPE; + + /** + * 矢量类型 + */ + @WireField( + tag = 1, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeType#ADAPTER" + ) + public final ShapeType type; + + /** + * 矢量参数 + * 渲染参数 + */ + @WireField( + tag = 10, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle#ADAPTER" + ) + public final ShapeStyle styles; + + /** + * 矢量图层 2D 变换矩阵 + */ + @WireField( + tag = 11, + adapter = "com.opensource.svgaplayer.proto.Transform#ADAPTER" + ) + public final Transform transform; + + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeArgs#ADAPTER" + ) + public final ShapeArgs shape; + + @WireField( + tag = 3, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$RectArgs#ADAPTER" + ) + public final RectArgs rect; + + @WireField( + tag = 4, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$EllipseArgs#ADAPTER" + ) + public final EllipseArgs ellipse; + + public ShapeEntity(ShapeType type, ShapeStyle styles, Transform transform, ShapeArgs shape, RectArgs rect, EllipseArgs ellipse) { + this(type, styles, transform, shape, rect, ellipse, ByteString.EMPTY); + } + + public ShapeEntity(ShapeType type, ShapeStyle styles, Transform transform, ShapeArgs shape, RectArgs rect, EllipseArgs ellipse, ByteString unknownFields) { + super(ADAPTER, unknownFields); + if (Internal.countNonNull(shape, rect, ellipse) > 1) { + throw new IllegalArgumentException("at most one of shape, rect, ellipse may be non-null"); + } + this.type = type; + this.styles = styles; + this.transform = transform; + this.shape = shape; + this.rect = rect; + this.ellipse = ellipse; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.type = type; + builder.styles = styles; + builder.transform = transform; + builder.shape = shape; + builder.rect = rect; + builder.ellipse = ellipse; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof ShapeEntity)) return false; + ShapeEntity o = (ShapeEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(type, o.type) + && Internal.equals(styles, o.styles) + && Internal.equals(transform, o.transform) + && Internal.equals(shape, o.shape) + && Internal.equals(rect, o.rect) + && Internal.equals(ellipse, o.ellipse); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (type != null ? type.hashCode() : 0); + result = result * 37 + (styles != null ? styles.hashCode() : 0); + result = result * 37 + (transform != null ? transform.hashCode() : 0); + result = result * 37 + (shape != null ? shape.hashCode() : 0); + result = result * 37 + (rect != null ? rect.hashCode() : 0); + result = result * 37 + (ellipse != null ? ellipse.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (type != null) builder.append(", type=").append(type); + if (styles != null) builder.append(", styles=").append(styles); + if (transform != null) builder.append(", transform=").append(transform); + if (shape != null) builder.append(", shape=").append(shape); + if (rect != null) builder.append(", rect=").append(rect); + if (ellipse != null) builder.append(", ellipse=").append(ellipse); + return builder.replace(0, 2, "ShapeEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public ShapeType type; + + public ShapeStyle styles; + + public Transform transform; + + public ShapeArgs shape; + + public RectArgs rect; + + public EllipseArgs ellipse; + + public Builder() { + } + + /** + * 矢量类型 + */ + public Builder type(ShapeType type) { + this.type = type; + return this; + } + + /** + * 矢量参数 + * 渲染参数 + */ + public Builder styles(ShapeStyle styles) { + this.styles = styles; + return this; + } + + /** + * 矢量图层 2D 变换矩阵 + */ + public Builder transform(Transform transform) { + this.transform = transform; + return this; + } + + public Builder shape(ShapeArgs shape) { + this.shape = shape; + this.rect = null; + this.ellipse = null; + return this; + } + + public Builder rect(RectArgs rect) { + this.rect = rect; + this.shape = null; + this.ellipse = null; + return this; + } + + public Builder ellipse(EllipseArgs ellipse) { + this.ellipse = ellipse; + this.shape = null; + this.rect = null; + return this; + } + + @Override + public ShapeEntity build() { + return new ShapeEntity(type, styles, transform, shape, rect, ellipse, super.buildUnknownFields()); + } + } + + public enum ShapeType implements WireEnum { + /** + * 路径 + */ + SHAPE(0), + + /** + * 矩形 + */ + RECT(1), + + /** + * 圆形 + */ + ELLIPSE(2), + + /** + * 与前帧一致 + */ + KEEP(3); + + public static final ProtoAdapter ADAPTER = ProtoAdapter.newEnumAdapter(ShapeType.class); + + private final int value; + + ShapeType(int value) { + this.value = value; + } + + /** + * Return the constant for {@code value} or null. + */ + public static ShapeType fromValue(int value) { + switch (value) { + case 0: return SHAPE; + case 1: return RECT; + case 2: return ELLIPSE; + case 3: return KEEP; + default: return null; + } + } + + @Override + public int getValue() { + return value; + } + } + + public static final class ShapeArgs extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_ShapeArgs(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_D = ""; + + /** + * SVG 路径 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String d; + + public ShapeArgs(String d) { + this(d, ByteString.EMPTY); + } + + public ShapeArgs(String d, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.d = d; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.d = d; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof ShapeArgs)) return false; + ShapeArgs o = (ShapeArgs) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(d, o.d); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (d != null ? d.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (d != null) builder.append(", d=").append(d); + return builder.replace(0, 2, "ShapeArgs{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String d; + + public Builder() { + } + + /** + * SVG 路径 + */ + public Builder d(String d) { + this.d = d; + return this; + } + + @Override + public ShapeArgs build() { + return new ShapeArgs(d, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_ShapeArgs extends ProtoAdapter { + ProtoAdapter_ShapeArgs() { + super(FieldEncoding.LENGTH_DELIMITED, ShapeArgs.class); + } + + @Override + public int encodedSize(ShapeArgs value) { + return (value.d != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.d) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, ShapeArgs value) throws IOException { + if (value.d != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.d); + writer.writeBytes(value.unknownFields()); + } + + @Override + public ShapeArgs decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.d(ProtoAdapter.STRING.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public ShapeArgs redact(ShapeArgs value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public static final class RectArgs extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_RectArgs(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_X = 0.0f; + + public static final Float DEFAULT_Y = 0.0f; + + public static final Float DEFAULT_WIDTH = 0.0f; + + public static final Float DEFAULT_HEIGHT = 0.0f; + + public static final Float DEFAULT_CORNERRADIUS = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float x; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float y; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float width; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float height; + + /** + * 圆角半径 + */ + @WireField( + tag = 5, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float cornerRadius; + + public RectArgs(Float x, Float y, Float width, Float height, Float cornerRadius) { + this(x, y, width, height, cornerRadius, ByteString.EMPTY); + } + + public RectArgs(Float x, Float y, Float width, Float height, Float cornerRadius, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.cornerRadius = cornerRadius; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.x = x; + builder.y = y; + builder.width = width; + builder.height = height; + builder.cornerRadius = cornerRadius; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof RectArgs)) return false; + RectArgs o = (RectArgs) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(x, o.x) + && Internal.equals(y, o.y) + && Internal.equals(width, o.width) + && Internal.equals(height, o.height) + && Internal.equals(cornerRadius, o.cornerRadius); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (x != null ? x.hashCode() : 0); + result = result * 37 + (y != null ? y.hashCode() : 0); + result = result * 37 + (width != null ? width.hashCode() : 0); + result = result * 37 + (height != null ? height.hashCode() : 0); + result = result * 37 + (cornerRadius != null ? cornerRadius.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (x != null) builder.append(", x=").append(x); + if (y != null) builder.append(", y=").append(y); + if (width != null) builder.append(", width=").append(width); + if (height != null) builder.append(", height=").append(height); + if (cornerRadius != null) builder.append(", cornerRadius=").append(cornerRadius); + return builder.replace(0, 2, "RectArgs{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float x; + + public Float y; + + public Float width; + + public Float height; + + public Float cornerRadius; + + public Builder() { + } + + public Builder x(Float x) { + this.x = x; + return this; + } + + public Builder y(Float y) { + this.y = y; + return this; + } + + public Builder width(Float width) { + this.width = width; + return this; + } + + public Builder height(Float height) { + this.height = height; + return this; + } + + /** + * 圆角半径 + */ + public Builder cornerRadius(Float cornerRadius) { + this.cornerRadius = cornerRadius; + return this; + } + + @Override + public RectArgs build() { + return new RectArgs(x, y, width, height, cornerRadius, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_RectArgs extends ProtoAdapter { + ProtoAdapter_RectArgs() { + super(FieldEncoding.LENGTH_DELIMITED, RectArgs.class); + } + + @Override + public int encodedSize(RectArgs value) { + return (value.x != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.x) : 0) + + (value.y != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.y) : 0) + + (value.width != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.width) : 0) + + (value.height != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.height) : 0) + + (value.cornerRadius != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(5, value.cornerRadius) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, RectArgs value) throws IOException { + if (value.x != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.x); + if (value.y != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.y); + if (value.width != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.width); + if (value.height != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.height); + if (value.cornerRadius != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 5, value.cornerRadius); + writer.writeBytes(value.unknownFields()); + } + + @Override + public RectArgs decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.x(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.y(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.width(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.height(ProtoAdapter.FLOAT.decode(reader)); break; + case 5: builder.cornerRadius(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public RectArgs redact(RectArgs value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public static final class EllipseArgs extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_EllipseArgs(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_X = 0.0f; + + public static final Float DEFAULT_Y = 0.0f; + + public static final Float DEFAULT_RADIUSX = 0.0f; + + public static final Float DEFAULT_RADIUSY = 0.0f; + + /** + * 圆中心点 X + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float x; + + /** + * 圆中心点 Y + */ + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float y; + + /** + * 横向半径 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float radiusX; + + /** + * 纵向半径 + */ + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float radiusY; + + public EllipseArgs(Float x, Float y, Float radiusX, Float radiusY) { + this(x, y, radiusX, radiusY, ByteString.EMPTY); + } + + public EllipseArgs(Float x, Float y, Float radiusX, Float radiusY, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.x = x; + this.y = y; + this.radiusX = radiusX; + this.radiusY = radiusY; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.x = x; + builder.y = y; + builder.radiusX = radiusX; + builder.radiusY = radiusY; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof EllipseArgs)) return false; + EllipseArgs o = (EllipseArgs) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(x, o.x) + && Internal.equals(y, o.y) + && Internal.equals(radiusX, o.radiusX) + && Internal.equals(radiusY, o.radiusY); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (x != null ? x.hashCode() : 0); + result = result * 37 + (y != null ? y.hashCode() : 0); + result = result * 37 + (radiusX != null ? radiusX.hashCode() : 0); + result = result * 37 + (radiusY != null ? radiusY.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (x != null) builder.append(", x=").append(x); + if (y != null) builder.append(", y=").append(y); + if (radiusX != null) builder.append(", radiusX=").append(radiusX); + if (radiusY != null) builder.append(", radiusY=").append(radiusY); + return builder.replace(0, 2, "EllipseArgs{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float x; + + public Float y; + + public Float radiusX; + + public Float radiusY; + + public Builder() { + } + + /** + * 圆中心点 X + */ + public Builder x(Float x) { + this.x = x; + return this; + } + + /** + * 圆中心点 Y + */ + public Builder y(Float y) { + this.y = y; + return this; + } + + /** + * 横向半径 + */ + public Builder radiusX(Float radiusX) { + this.radiusX = radiusX; + return this; + } + + /** + * 纵向半径 + */ + public Builder radiusY(Float radiusY) { + this.radiusY = radiusY; + return this; + } + + @Override + public EllipseArgs build() { + return new EllipseArgs(x, y, radiusX, radiusY, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_EllipseArgs extends ProtoAdapter { + ProtoAdapter_EllipseArgs() { + super(FieldEncoding.LENGTH_DELIMITED, EllipseArgs.class); + } + + @Override + public int encodedSize(EllipseArgs value) { + return (value.x != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.x) : 0) + + (value.y != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.y) : 0) + + (value.radiusX != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.radiusX) : 0) + + (value.radiusY != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.radiusY) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, EllipseArgs value) throws IOException { + if (value.x != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.x); + if (value.y != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.y); + if (value.radiusX != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.radiusX); + if (value.radiusY != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.radiusY); + writer.writeBytes(value.unknownFields()); + } + + @Override + public EllipseArgs decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.x(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.y(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.radiusX(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.radiusY(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public EllipseArgs redact(EllipseArgs value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public static final class ShapeStyle extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_ShapeStyle(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_STROKEWIDTH = 0.0f; + + public static final LineCap DEFAULT_LINECAP = LineCap.LineCap_BUTT; + + public static final LineJoin DEFAULT_LINEJOIN = LineJoin.LineJoin_MITER; + + public static final Float DEFAULT_MITERLIMIT = 0.0f; + + public static final Float DEFAULT_LINEDASHI = 0.0f; + + public static final Float DEFAULT_LINEDASHII = 0.0f; + + public static final Float DEFAULT_LINEDASHIII = 0.0f; + + /** + * 填充色 + */ + @WireField( + tag = 1, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$RGBAColor#ADAPTER" + ) + public final RGBAColor fill; + + /** + * 描边色 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$RGBAColor#ADAPTER" + ) + public final RGBAColor stroke; + + /** + * 描边宽 + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float strokeWidth; + + /** + * 线段端点样式 + */ + @WireField( + tag = 4, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$LineCap#ADAPTER" + ) + public final LineCap lineCap; + + /** + * 线段连接样式 + */ + @WireField( + tag = 5, + adapter = "com.opensource.svgaplayer.proto.ShapeEntity$ShapeStyle$LineJoin#ADAPTER" + ) + public final LineJoin lineJoin; + + /** + * 尖角限制 + */ + @WireField( + tag = 6, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float miterLimit; + + /** + * 虚线参数 Dash + */ + @WireField( + tag = 7, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float lineDashI; + + /** + * 虚线参数 Gap + */ + @WireField( + tag = 8, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float lineDashII; + + /** + * 虚线参数 Offset + */ + @WireField( + tag = 9, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float lineDashIII; + + public ShapeStyle(RGBAColor fill, RGBAColor stroke, Float strokeWidth, LineCap lineCap, LineJoin lineJoin, Float miterLimit, Float lineDashI, Float lineDashII, Float lineDashIII) { + this(fill, stroke, strokeWidth, lineCap, lineJoin, miterLimit, lineDashI, lineDashII, lineDashIII, ByteString.EMPTY); + } + + public ShapeStyle(RGBAColor fill, RGBAColor stroke, Float strokeWidth, LineCap lineCap, LineJoin lineJoin, Float miterLimit, Float lineDashI, Float lineDashII, Float lineDashIII, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.fill = fill; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + this.lineCap = lineCap; + this.lineJoin = lineJoin; + this.miterLimit = miterLimit; + this.lineDashI = lineDashI; + this.lineDashII = lineDashII; + this.lineDashIII = lineDashIII; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.fill = fill; + builder.stroke = stroke; + builder.strokeWidth = strokeWidth; + builder.lineCap = lineCap; + builder.lineJoin = lineJoin; + builder.miterLimit = miterLimit; + builder.lineDashI = lineDashI; + builder.lineDashII = lineDashII; + builder.lineDashIII = lineDashIII; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof ShapeStyle)) return false; + ShapeStyle o = (ShapeStyle) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(fill, o.fill) + && Internal.equals(stroke, o.stroke) + && Internal.equals(strokeWidth, o.strokeWidth) + && Internal.equals(lineCap, o.lineCap) + && Internal.equals(lineJoin, o.lineJoin) + && Internal.equals(miterLimit, o.miterLimit) + && Internal.equals(lineDashI, o.lineDashI) + && Internal.equals(lineDashII, o.lineDashII) + && Internal.equals(lineDashIII, o.lineDashIII); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (fill != null ? fill.hashCode() : 0); + result = result * 37 + (stroke != null ? stroke.hashCode() : 0); + result = result * 37 + (strokeWidth != null ? strokeWidth.hashCode() : 0); + result = result * 37 + (lineCap != null ? lineCap.hashCode() : 0); + result = result * 37 + (lineJoin != null ? lineJoin.hashCode() : 0); + result = result * 37 + (miterLimit != null ? miterLimit.hashCode() : 0); + result = result * 37 + (lineDashI != null ? lineDashI.hashCode() : 0); + result = result * 37 + (lineDashII != null ? lineDashII.hashCode() : 0); + result = result * 37 + (lineDashIII != null ? lineDashIII.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (fill != null) builder.append(", fill=").append(fill); + if (stroke != null) builder.append(", stroke=").append(stroke); + if (strokeWidth != null) builder.append(", strokeWidth=").append(strokeWidth); + if (lineCap != null) builder.append(", lineCap=").append(lineCap); + if (lineJoin != null) builder.append(", lineJoin=").append(lineJoin); + if (miterLimit != null) builder.append(", miterLimit=").append(miterLimit); + if (lineDashI != null) builder.append(", lineDashI=").append(lineDashI); + if (lineDashII != null) builder.append(", lineDashII=").append(lineDashII); + if (lineDashIII != null) builder.append(", lineDashIII=").append(lineDashIII); + return builder.replace(0, 2, "ShapeStyle{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public RGBAColor fill; + + public RGBAColor stroke; + + public Float strokeWidth; + + public LineCap lineCap; + + public LineJoin lineJoin; + + public Float miterLimit; + + public Float lineDashI; + + public Float lineDashII; + + public Float lineDashIII; + + public Builder() { + } + + /** + * 填充色 + */ + public Builder fill(RGBAColor fill) { + this.fill = fill; + return this; + } + + /** + * 描边色 + */ + public Builder stroke(RGBAColor stroke) { + this.stroke = stroke; + return this; + } + + /** + * 描边宽 + */ + public Builder strokeWidth(Float strokeWidth) { + this.strokeWidth = strokeWidth; + return this; + } + + /** + * 线段端点样式 + */ + public Builder lineCap(LineCap lineCap) { + this.lineCap = lineCap; + return this; + } + + /** + * 线段连接样式 + */ + public Builder lineJoin(LineJoin lineJoin) { + this.lineJoin = lineJoin; + return this; + } + + /** + * 尖角限制 + */ + public Builder miterLimit(Float miterLimit) { + this.miterLimit = miterLimit; + return this; + } + + /** + * 虚线参数 Dash + */ + public Builder lineDashI(Float lineDashI) { + this.lineDashI = lineDashI; + return this; + } + + /** + * 虚线参数 Gap + */ + public Builder lineDashII(Float lineDashII) { + this.lineDashII = lineDashII; + return this; + } + + /** + * 虚线参数 Offset + */ + public Builder lineDashIII(Float lineDashIII) { + this.lineDashIII = lineDashIII; + return this; + } + + @Override + public ShapeStyle build() { + return new ShapeStyle(fill, stroke, strokeWidth, lineCap, lineJoin, miterLimit, lineDashI, lineDashII, lineDashIII, super.buildUnknownFields()); + } + } + + public static final class RGBAColor extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_RGBAColor(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_R = 0.0f; + + public static final Float DEFAULT_G = 0.0f; + + public static final Float DEFAULT_B = 0.0f; + + public static final Float DEFAULT_A = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float r; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float g; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float b; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float a; + + public RGBAColor(Float r, Float g, Float b, Float a) { + this(r, g, b, a, ByteString.EMPTY); + } + + public RGBAColor(Float r, Float g, Float b, Float a, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.r = r; + builder.g = g; + builder.b = b; + builder.a = a; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof RGBAColor)) return false; + RGBAColor o = (RGBAColor) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(r, o.r) + && Internal.equals(g, o.g) + && Internal.equals(b, o.b) + && Internal.equals(a, o.a); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (r != null ? r.hashCode() : 0); + result = result * 37 + (g != null ? g.hashCode() : 0); + result = result * 37 + (b != null ? b.hashCode() : 0); + result = result * 37 + (a != null ? a.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (r != null) builder.append(", r=").append(r); + if (g != null) builder.append(", g=").append(g); + if (b != null) builder.append(", b=").append(b); + if (a != null) builder.append(", a=").append(a); + return builder.replace(0, 2, "RGBAColor{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float r; + + public Float g; + + public Float b; + + public Float a; + + public Builder() { + } + + public Builder r(Float r) { + this.r = r; + return this; + } + + public Builder g(Float g) { + this.g = g; + return this; + } + + public Builder b(Float b) { + this.b = b; + return this; + } + + public Builder a(Float a) { + this.a = a; + return this; + } + + @Override + public RGBAColor build() { + return new RGBAColor(r, g, b, a, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_RGBAColor extends ProtoAdapter { + ProtoAdapter_RGBAColor() { + super(FieldEncoding.LENGTH_DELIMITED, RGBAColor.class); + } + + @Override + public int encodedSize(RGBAColor value) { + return (value.r != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.r) : 0) + + (value.g != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.g) : 0) + + (value.b != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.b) : 0) + + (value.a != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.a) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, RGBAColor value) throws IOException { + if (value.r != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.r); + if (value.g != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.g); + if (value.b != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.b); + if (value.a != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.a); + writer.writeBytes(value.unknownFields()); + } + + @Override + public RGBAColor decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.r(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.g(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.b(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.a(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public RGBAColor redact(RGBAColor value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + public enum LineCap implements WireEnum { + LineCap_BUTT(0), + + LineCap_ROUND(1), + + LineCap_SQUARE(2); + + public static final ProtoAdapter ADAPTER = ProtoAdapter.newEnumAdapter(LineCap.class); + + private final int value; + + LineCap(int value) { + this.value = value; + } + + /** + * Return the constant for {@code value} or null. + */ + public static LineCap fromValue(int value) { + switch (value) { + case 0: return LineCap_BUTT; + case 1: return LineCap_ROUND; + case 2: return LineCap_SQUARE; + default: return null; + } + } + + @Override + public int getValue() { + return value; + } + } + + public enum LineJoin implements WireEnum { + LineJoin_MITER(0), + + LineJoin_ROUND(1), + + LineJoin_BEVEL(2); + + public static final ProtoAdapter ADAPTER = ProtoAdapter.newEnumAdapter(LineJoin.class); + + private final int value; + + LineJoin(int value) { + this.value = value; + } + + /** + * Return the constant for {@code value} or null. + */ + public static LineJoin fromValue(int value) { + switch (value) { + case 0: return LineJoin_MITER; + case 1: return LineJoin_ROUND; + case 2: return LineJoin_BEVEL; + default: return null; + } + } + + @Override + public int getValue() { + return value; + } + } + + private static final class ProtoAdapter_ShapeStyle extends ProtoAdapter { + ProtoAdapter_ShapeStyle() { + super(FieldEncoding.LENGTH_DELIMITED, ShapeStyle.class); + } + + @Override + public int encodedSize(ShapeStyle value) { + return (value.fill != null ? RGBAColor.ADAPTER.encodedSizeWithTag(1, value.fill) : 0) + + (value.stroke != null ? RGBAColor.ADAPTER.encodedSizeWithTag(2, value.stroke) : 0) + + (value.strokeWidth != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.strokeWidth) : 0) + + (value.lineCap != null ? LineCap.ADAPTER.encodedSizeWithTag(4, value.lineCap) : 0) + + (value.lineJoin != null ? LineJoin.ADAPTER.encodedSizeWithTag(5, value.lineJoin) : 0) + + (value.miterLimit != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(6, value.miterLimit) : 0) + + (value.lineDashI != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(7, value.lineDashI) : 0) + + (value.lineDashII != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(8, value.lineDashII) : 0) + + (value.lineDashIII != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(9, value.lineDashIII) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, ShapeStyle value) throws IOException { + if (value.fill != null) RGBAColor.ADAPTER.encodeWithTag(writer, 1, value.fill); + if (value.stroke != null) RGBAColor.ADAPTER.encodeWithTag(writer, 2, value.stroke); + if (value.strokeWidth != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.strokeWidth); + if (value.lineCap != null) LineCap.ADAPTER.encodeWithTag(writer, 4, value.lineCap); + if (value.lineJoin != null) LineJoin.ADAPTER.encodeWithTag(writer, 5, value.lineJoin); + if (value.miterLimit != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 6, value.miterLimit); + if (value.lineDashI != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 7, value.lineDashI); + if (value.lineDashII != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 8, value.lineDashII); + if (value.lineDashIII != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 9, value.lineDashIII); + writer.writeBytes(value.unknownFields()); + } + + @Override + public ShapeStyle decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.fill(RGBAColor.ADAPTER.decode(reader)); break; + case 2: builder.stroke(RGBAColor.ADAPTER.decode(reader)); break; + case 3: builder.strokeWidth(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: { + try { + builder.lineCap(LineCap.ADAPTER.decode(reader)); + } catch (EnumConstantNotFoundException e) { + builder.addUnknownField(tag, FieldEncoding.VARINT, (long) e.value); + } + break; + } + case 5: { + try { + builder.lineJoin(LineJoin.ADAPTER.decode(reader)); + } catch (EnumConstantNotFoundException e) { + builder.addUnknownField(tag, FieldEncoding.VARINT, (long) e.value); + } + break; + } + case 6: builder.miterLimit(ProtoAdapter.FLOAT.decode(reader)); break; + case 7: builder.lineDashI(ProtoAdapter.FLOAT.decode(reader)); break; + case 8: builder.lineDashII(ProtoAdapter.FLOAT.decode(reader)); break; + case 9: builder.lineDashIII(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public ShapeStyle redact(ShapeStyle value) { + Builder builder = value.newBuilder(); + if (builder.fill != null) builder.fill = RGBAColor.ADAPTER.redact(builder.fill); + if (builder.stroke != null) builder.stroke = RGBAColor.ADAPTER.redact(builder.stroke); + builder.clearUnknownFields(); + return builder.build(); + } + } + } + + private static final class ProtoAdapter_ShapeEntity extends ProtoAdapter { + ProtoAdapter_ShapeEntity() { + super(FieldEncoding.LENGTH_DELIMITED, ShapeEntity.class); + } + + @Override + public int encodedSize(ShapeEntity value) { + return (value.type != null ? ShapeType.ADAPTER.encodedSizeWithTag(1, value.type) : 0) + + (value.styles != null ? ShapeStyle.ADAPTER.encodedSizeWithTag(10, value.styles) : 0) + + (value.transform != null ? Transform.ADAPTER.encodedSizeWithTag(11, value.transform) : 0) + + (value.shape != null ? ShapeArgs.ADAPTER.encodedSizeWithTag(2, value.shape) : 0) + + (value.rect != null ? RectArgs.ADAPTER.encodedSizeWithTag(3, value.rect) : 0) + + (value.ellipse != null ? EllipseArgs.ADAPTER.encodedSizeWithTag(4, value.ellipse) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, ShapeEntity value) throws IOException { + if (value.type != null) ShapeType.ADAPTER.encodeWithTag(writer, 1, value.type); + if (value.styles != null) ShapeStyle.ADAPTER.encodeWithTag(writer, 10, value.styles); + if (value.transform != null) Transform.ADAPTER.encodeWithTag(writer, 11, value.transform); + if (value.shape != null) ShapeArgs.ADAPTER.encodeWithTag(writer, 2, value.shape); + if (value.rect != null) RectArgs.ADAPTER.encodeWithTag(writer, 3, value.rect); + if (value.ellipse != null) EllipseArgs.ADAPTER.encodeWithTag(writer, 4, value.ellipse); + writer.writeBytes(value.unknownFields()); + } + + @Override + public ShapeEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: { + try { + builder.type(ShapeType.ADAPTER.decode(reader)); + } catch (EnumConstantNotFoundException e) { + builder.addUnknownField(tag, FieldEncoding.VARINT, (long) e.value); + } + break; + } + case 10: builder.styles(ShapeStyle.ADAPTER.decode(reader)); break; + case 11: builder.transform(Transform.ADAPTER.decode(reader)); break; + case 2: builder.shape(ShapeArgs.ADAPTER.decode(reader)); break; + case 3: builder.rect(RectArgs.ADAPTER.decode(reader)); break; + case 4: builder.ellipse(EllipseArgs.ADAPTER.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public ShapeEntity redact(ShapeEntity value) { + Builder builder = value.newBuilder(); + if (builder.styles != null) builder.styles = ShapeStyle.ADAPTER.redact(builder.styles); + if (builder.transform != null) builder.transform = Transform.ADAPTER.redact(builder.transform); + if (builder.shape != null) builder.shape = ShapeArgs.ADAPTER.redact(builder.shape); + if (builder.rect != null) builder.rect = RectArgs.ADAPTER.redact(builder.rect); + if (builder.ellipse != null) builder.ellipse = EllipseArgs.ADAPTER.redact(builder.ellipse); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java new file mode 100644 index 000000000..bc9120b54 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/SpriteEntity.java @@ -0,0 +1,202 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 13:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import okio.ByteString; + +public final class SpriteEntity extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_SpriteEntity(); + + private static final long serialVersionUID = 0L; + + public static final String DEFAULT_IMAGEKEY = ""; + + public static final String DEFAULT_MATTEKEY = ""; + + /** + * 元件所对应的位图键名, 如果 imageKey 含有 .vector 后缀,该 sprite 为矢量图层 含有 .matte 后缀,该 sprite 为遮罩图层。 + */ + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String imageKey; + + /** + * 帧列表 + */ + @WireField( + tag = 2, + adapter = "com.opensource.svgaplayer.proto.FrameEntity#ADAPTER", + label = WireField.Label.REPEATED + ) + public final List frames; + + /** + * 被遮罩图层的 matteKey 对应的是其遮罩图层的 imageKey. + */ + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#STRING" + ) + public final String matteKey; + + public SpriteEntity(String imageKey, List frames, String matteKey) { + this(imageKey, frames, matteKey, ByteString.EMPTY); + } + + public SpriteEntity(String imageKey, List frames, String matteKey, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.imageKey = imageKey; + this.frames = Internal.immutableCopyOf("frames", frames); + this.matteKey = matteKey; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.imageKey = imageKey; + builder.frames = Internal.copyOf("frames", frames); + builder.matteKey = matteKey; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof SpriteEntity)) return false; + SpriteEntity o = (SpriteEntity) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(imageKey, o.imageKey) + && frames.equals(o.frames) + && Internal.equals(matteKey, o.matteKey); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (imageKey != null ? imageKey.hashCode() : 0); + result = result * 37 + frames.hashCode(); + result = result * 37 + (matteKey != null ? matteKey.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (imageKey != null) builder.append(", imageKey=").append(imageKey); + if (!frames.isEmpty()) builder.append(", frames=").append(frames); + if (matteKey != null) builder.append(", matteKey=").append(matteKey); + return builder.replace(0, 2, "SpriteEntity{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public String imageKey; + + public List frames; + + public String matteKey; + + public Builder() { + frames = Internal.newMutableList(); + } + + /** + * 元件所对应的位图键名, 如果 imageKey 含有 .vector 后缀,该 sprite 为矢量图层 含有 .matte 后缀,该 sprite 为遮罩图层。 + */ + public Builder imageKey(String imageKey) { + this.imageKey = imageKey; + return this; + } + + /** + * 帧列表 + */ + public Builder frames(List frames) { + Internal.checkElementsNotNull(frames); + this.frames = frames; + return this; + } + + /** + * 被遮罩图层的 matteKey 对应的是其遮罩图层的 imageKey. + */ + public Builder matteKey(String matteKey) { + this.matteKey = matteKey; + return this; + } + + @Override + public SpriteEntity build() { + return new SpriteEntity(imageKey, frames, matteKey, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_SpriteEntity extends ProtoAdapter { + ProtoAdapter_SpriteEntity() { + super(FieldEncoding.LENGTH_DELIMITED, SpriteEntity.class); + } + + @Override + public int encodedSize(SpriteEntity value) { + return (value.imageKey != null ? ProtoAdapter.STRING.encodedSizeWithTag(1, value.imageKey) : 0) + + FrameEntity.ADAPTER.asRepeated().encodedSizeWithTag(2, value.frames) + + (value.matteKey != null ? ProtoAdapter.STRING.encodedSizeWithTag(3, value.matteKey) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, SpriteEntity value) throws IOException { + if (value.imageKey != null) ProtoAdapter.STRING.encodeWithTag(writer, 1, value.imageKey); + FrameEntity.ADAPTER.asRepeated().encodeWithTag(writer, 2, value.frames); + if (value.matteKey != null) ProtoAdapter.STRING.encodeWithTag(writer, 3, value.matteKey); + writer.writeBytes(value.unknownFields()); + } + + @Override + public SpriteEntity decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.imageKey(ProtoAdapter.STRING.decode(reader)); break; + case 2: builder.frames.add(FrameEntity.ADAPTER.decode(reader)); break; + case 3: builder.matteKey(ProtoAdapter.STRING.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public SpriteEntity redact(SpriteEntity value) { + Builder builder = value.newBuilder(); + Internal.redactElements(builder.frames, FrameEntity.ADAPTER); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/Transform.java b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/Transform.java new file mode 100644 index 000000000..c4c4941d4 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/proto/Transform.java @@ -0,0 +1,251 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: svga.proto at 34:1 +package com.opensource.svgaplayer.proto; + +import com.squareup.wire.FieldEncoding; +import com.squareup.wire.Message; +import com.squareup.wire.ProtoAdapter; +import com.squareup.wire.ProtoReader; +import com.squareup.wire.ProtoWriter; +import com.squareup.wire.WireField; +import com.squareup.wire.internal.Internal; +import java.io.IOException; +import java.lang.Float; +import java.lang.Object; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import okio.ByteString; + +public final class Transform extends Message { + public static final ProtoAdapter ADAPTER = new ProtoAdapter_Transform(); + + private static final long serialVersionUID = 0L; + + public static final Float DEFAULT_A = 0.0f; + + public static final Float DEFAULT_B = 0.0f; + + public static final Float DEFAULT_C = 0.0f; + + public static final Float DEFAULT_D = 0.0f; + + public static final Float DEFAULT_TX = 0.0f; + + public static final Float DEFAULT_TY = 0.0f; + + @WireField( + tag = 1, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float a; + + @WireField( + tag = 2, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float b; + + @WireField( + tag = 3, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float c; + + @WireField( + tag = 4, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float d; + + @WireField( + tag = 5, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float tx; + + @WireField( + tag = 6, + adapter = "com.squareup.wire.ProtoAdapter#FLOAT" + ) + public final Float ty; + + public Transform(Float a, Float b, Float c, Float d, Float tx, Float ty) { + this(a, b, c, d, tx, ty, ByteString.EMPTY); + } + + public Transform(Float a, Float b, Float c, Float d, Float tx, Float ty, ByteString unknownFields) { + super(ADAPTER, unknownFields); + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.tx = tx; + this.ty = ty; + } + + @Override + public Builder newBuilder() { + Builder builder = new Builder(); + builder.a = a; + builder.b = b; + builder.c = c; + builder.d = d; + builder.tx = tx; + builder.ty = ty; + builder.addUnknownFields(unknownFields()); + return builder; + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof Transform)) return false; + Transform o = (Transform) other; + return unknownFields().equals(o.unknownFields()) + && Internal.equals(a, o.a) + && Internal.equals(b, o.b) + && Internal.equals(c, o.c) + && Internal.equals(d, o.d) + && Internal.equals(tx, o.tx) + && Internal.equals(ty, o.ty); + } + + @Override + public int hashCode() { + int result = super.hashCode; + if (result == 0) { + result = unknownFields().hashCode(); + result = result * 37 + (a != null ? a.hashCode() : 0); + result = result * 37 + (b != null ? b.hashCode() : 0); + result = result * 37 + (c != null ? c.hashCode() : 0); + result = result * 37 + (d != null ? d.hashCode() : 0); + result = result * 37 + (tx != null ? tx.hashCode() : 0); + result = result * 37 + (ty != null ? ty.hashCode() : 0); + super.hashCode = result; + } + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (a != null) builder.append(", a=").append(a); + if (b != null) builder.append(", b=").append(b); + if (c != null) builder.append(", c=").append(c); + if (d != null) builder.append(", d=").append(d); + if (tx != null) builder.append(", tx=").append(tx); + if (ty != null) builder.append(", ty=").append(ty); + return builder.replace(0, 2, "Transform{").append('}').toString(); + } + + public static final class Builder extends Message.Builder { + public Float a; + + public Float b; + + public Float c; + + public Float d; + + public Float tx; + + public Float ty; + + public Builder() { + } + + public Builder a(Float a) { + this.a = a; + return this; + } + + public Builder b(Float b) { + this.b = b; + return this; + } + + public Builder c(Float c) { + this.c = c; + return this; + } + + public Builder d(Float d) { + this.d = d; + return this; + } + + public Builder tx(Float tx) { + this.tx = tx; + return this; + } + + public Builder ty(Float ty) { + this.ty = ty; + return this; + } + + @Override + public Transform build() { + return new Transform(a, b, c, d, tx, ty, super.buildUnknownFields()); + } + } + + private static final class ProtoAdapter_Transform extends ProtoAdapter { + ProtoAdapter_Transform() { + super(FieldEncoding.LENGTH_DELIMITED, Transform.class); + } + + @Override + public int encodedSize(Transform value) { + return (value.a != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(1, value.a) : 0) + + (value.b != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(2, value.b) : 0) + + (value.c != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(3, value.c) : 0) + + (value.d != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(4, value.d) : 0) + + (value.tx != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(5, value.tx) : 0) + + (value.ty != null ? ProtoAdapter.FLOAT.encodedSizeWithTag(6, value.ty) : 0) + + value.unknownFields().size(); + } + + @Override + public void encode(ProtoWriter writer, Transform value) throws IOException { + if (value.a != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 1, value.a); + if (value.b != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 2, value.b); + if (value.c != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 3, value.c); + if (value.d != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 4, value.d); + if (value.tx != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 5, value.tx); + if (value.ty != null) ProtoAdapter.FLOAT.encodeWithTag(writer, 6, value.ty); + writer.writeBytes(value.unknownFields()); + } + + @Override + public Transform decode(ProtoReader reader) throws IOException { + Builder builder = new Builder(); + long token = reader.beginMessage(); + for (int tag; (tag = reader.nextTag()) != -1;) { + switch (tag) { + case 1: builder.a(ProtoAdapter.FLOAT.decode(reader)); break; + case 2: builder.b(ProtoAdapter.FLOAT.decode(reader)); break; + case 3: builder.c(ProtoAdapter.FLOAT.decode(reader)); break; + case 4: builder.d(ProtoAdapter.FLOAT.decode(reader)); break; + case 5: builder.tx(ProtoAdapter.FLOAT.decode(reader)); break; + case 6: builder.ty(ProtoAdapter.FLOAT.decode(reader)); break; + default: { + FieldEncoding fieldEncoding = reader.peekFieldEncoding(); + Object value = fieldEncoding.rawProtoAdapter().decode(reader); + builder.addUnknownField(tag, fieldEncoding, value); + } + } + } + reader.endMessage(token); + return builder.build(); + } + + @Override + public Transform redact(Transform value) { + Builder builder = value.newBuilder(); + builder.clearUnknownFields(); + return builder.build(); + } + } +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/Pools.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/Pools.kt new file mode 100644 index 000000000..7382ab84b --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/Pools.kt @@ -0,0 +1,102 @@ +package com.opensource.svgaplayer.utils + +/** + * Helper class for creating pools of objects. An example use looks like this: + *
+ * public class MyPooledClass {
+ *
+ *     private static final SynchronizedPool sPool =
+ *             new SynchronizedPool(10);
+ *
+ *     public static MyPooledClass obtain() {
+ *         MyPooledClass instance = sPool.acquire();
+ *         return (instance != null) ? instance : new MyPooledClass();
+ *     }
+ *
+ *     public void recycle() {
+ *          // Clear state if needed.
+ *          sPool.release(this);
+ *     }
+ *
+ *     . . .
+ * }
+ * 
+ * + */ +class Pools private constructor() { + + /** + * Interface for managing a pool of objects. + * + * @param The pooled type. + */ + interface Pool { + /** + * @return An instance from the pool if such, null otherwise. + */ + fun acquire(): T? + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + fun release(instance: T): Boolean + } + + /** + * Simple (non-synchronized) pool of objects. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + * + * @param The pooled type. + */ + open class SimplePool(maxPoolSize: Int) : Pool { + private val mPool: Array + private var mPoolSize = 0 + + init { + require(maxPoolSize > 0) { "The max pool size must be > 0" } + mPool = arrayOfNulls(maxPoolSize) + } + + @Suppress("UNCHECKED_CAST") + override fun acquire(): T? { + if (mPoolSize > 0) { + val lastPooledIndex = mPoolSize - 1 + val instance = mPool[lastPooledIndex] as T? + mPool[lastPooledIndex] = null + mPoolSize-- + return instance + } + return null + } + + override fun release(instance: T): Boolean { + check(!isInPool(instance)) { "Already in the pool!" } + if (mPoolSize < mPool.size) { + mPool[mPoolSize] = instance + mPoolSize++ + return true + } + return false + } + + private fun isInPool(instance: T): Boolean { + for (i in 0 until mPoolSize) { + if (mPool[i] === instance) { + return true + } + } + return false + } + + } + + +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt new file mode 100644 index 000000000..792abc266 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/SVGAScaleInfo.kt @@ -0,0 +1,146 @@ +package com.opensource.svgaplayer.utils + +import android.widget.ImageView + +/** + * Created by ubt on 2018/1/19. + */ +class SVGAScaleInfo { + + var tranFx : Float = 0.0f + var tranFy : Float = 0.0f + var scaleFx : Float = 1.0f + var scaleFy : Float = 1.0f + var ratio = 1.0f + var ratioX = false + + private fun resetVar(){ + tranFx = 0.0f + tranFy = 0.0f + scaleFx = 1.0f + scaleFy = 1.0f + ratio = 1.0f + ratioX = false + } + + fun performScaleType(canvasWidth : Float, canvasHeight: Float, videoWidth : Float, videoHeight : Float, scaleType: ImageView.ScaleType) { + if (canvasWidth == 0.0f || canvasHeight == 0.0f || videoWidth == 0.0f || videoHeight == 0.0f) { + return + } + + resetVar() + val canW_vidW_f = (canvasWidth - videoWidth) / 2.0f + val canH_vidH_f = (canvasHeight - videoHeight) / 2.0f + + val videoRatio = videoWidth / videoHeight + val canvasRatio = canvasWidth / canvasHeight + + val canH_d_vidH = canvasHeight / videoHeight + val canW_d_vidW = canvasWidth / videoWidth + + when (scaleType) { + ImageView.ScaleType.CENTER -> { + tranFx = canW_vidW_f + tranFy = canH_vidH_f + } + ImageView.ScaleType.CENTER_CROP -> { + if (videoRatio > canvasRatio) { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = (canvasWidth - videoWidth * (canH_d_vidH)) / 2.0f + } + else { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy = (canvasHeight - videoHeight * (canW_d_vidW)) / 2.0f + } + } + ImageView.ScaleType.CENTER_INSIDE -> { + if (videoWidth < canvasWidth && videoHeight < canvasHeight) { + tranFx = canW_vidW_f + tranFy = canH_vidH_f + } + else { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy = (canvasHeight - videoHeight * (canW_d_vidW)) / 2.0f + + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = (canvasWidth - videoWidth * (canH_d_vidH)) / 2.0f + } + } + } + ImageView.ScaleType.FIT_CENTER -> { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy = (canvasHeight - videoHeight * (canW_d_vidW)) / 2.0f + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = (canvasWidth - videoWidth * (canH_d_vidH)) / 2.0f + } + } + ImageView.ScaleType.FIT_START -> { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + } + } + ImageView.ScaleType.FIT_END -> { + if (videoRatio > canvasRatio) { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + tranFy= canvasHeight - videoHeight * (canW_d_vidW) + } + else { + ratio = canH_d_vidH + ratioX = false + scaleFx = canH_d_vidH + scaleFy = canH_d_vidH + tranFx = canvasWidth - videoWidth * (canH_d_vidH) + } + } + ImageView.ScaleType.FIT_XY -> { + ratio = Math.max(canW_d_vidW, canH_d_vidH) + ratioX = canW_d_vidW > canH_d_vidH + scaleFx = canW_d_vidW + scaleFy = canH_d_vidH + } + else -> { + ratio = canW_d_vidW + ratioX = true + scaleFx = canW_d_vidW + scaleFy = canW_d_vidW + } + } + } + +} diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt new file mode 100644 index 000000000..f87b30db0 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/SVGAStructs.kt @@ -0,0 +1,11 @@ +package com.opensource.svgaplayer.utils + +/** + * Created by cuiminghui on 2017/3/29. + */ + +class SVGAPoint(val x: Float, val y: Float, val value: Float) + +class SVGARect(val x: Double, val y: Double, val width: Double, val height: Double) + +class SVGARange(val location: Int, val length: Int) \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt new file mode 100644 index 000000000..33200b0e1 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/DefaultLogCat.kt @@ -0,0 +1,28 @@ +package com.opensource.svgaplayer.utils.log + +import android.util.Log + +/** + * 内部默认 ILogger 接口实现 + */ +class DefaultLogCat : ILogger { + override fun verbose(tag: String, msg: String) { + Log.v(tag, msg) + } + + override fun info(tag: String, msg: String) { + Log.i(tag, msg) + } + + override fun debug(tag: String, msg: String) { + Log.d(tag, msg) + } + + override fun warn(tag: String, msg: String) { + Log.w(tag, msg) + } + + override fun error(tag: String, msg: String?, error: Throwable?) { + Log.e(tag, msg, error) + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt new file mode 100644 index 000000000..ad935104c --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/ILogger.kt @@ -0,0 +1,12 @@ +package com.opensource.svgaplayer.utils.log + +/** + * log 外部接管接口 + **/ +interface ILogger { + fun verbose(tag: String, msg: String) + fun info(tag: String, msg: String) + fun debug(tag: String, msg: String) + fun warn(tag: String, msg: String) + fun error(tag: String, msg: String?, error: Throwable?) +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt new file mode 100644 index 000000000..60c67f9c2 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/LogUtils.kt @@ -0,0 +1,57 @@ +package com.opensource.svgaplayer.utils.log + +/** + * 日志输出 + */ +internal object LogUtils { + private const val TAG = "SVGALog" + + fun verbose(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.verbose(tag, msg) + } + + fun info(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.info(tag, msg) + } + + fun debug(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.debug(tag, msg) + } + + fun warn(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.warn(tag, msg) + } + + fun error(tag: String = TAG, msg: String) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.error(tag, msg, null) + } + + fun error(tag: String, error: Throwable) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.error(tag, error.message, error) + } + + fun error(tag: String = TAG, msg: String, error: Throwable) { + if (!SVGALogger.isLogEnabled()) { + return + } + SVGALogger.getSVGALogger()?.error(tag, msg, error) + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt new file mode 100644 index 000000000..5767c6385 --- /dev/null +++ b/SVGAlibrary/src/main/java/com/opensource/svgaplayer/utils/log/SVGALogger.kt @@ -0,0 +1,40 @@ +package com.opensource.svgaplayer.utils.log + +/** + * SVGA logger 配置管理 + **/ +object SVGALogger { + + private var mLogger: ILogger? = DefaultLogCat() + private var isLogEnabled = false + + /** + * log 接管注入 + */ + fun injectSVGALoggerImp(logImp: ILogger): SVGALogger { + mLogger = logImp + return this + } + + /** + * 设置是否开启 log + */ + fun setLogEnabled(isEnabled: Boolean): SVGALogger { + isLogEnabled = isEnabled + return this + } + + /** + * 获取当前 ILogger 实现类 + */ + fun getSVGALogger(): ILogger? { + return mLogger + } + + /** + * 是否开启 log + */ + fun isLogEnabled(): Boolean { + return isLogEnabled + } +} \ No newline at end of file diff --git a/SVGAlibrary/src/main/res/values/attrs.xml b/SVGAlibrary/src/main/res/values/attrs.xml new file mode 100644 index 000000000..471c34453 --- /dev/null +++ b/SVGAlibrary/src/main/res/values/attrs.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SVGAlibrary/src/main/res/values/strings.xml b/SVGAlibrary/src/main/res/values/strings.xml new file mode 100644 index 000000000..475f55349 --- /dev/null +++ b/SVGAlibrary/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SVGAPlayer + diff --git a/TabLayout/.gitignore b/TabLayout/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/TabLayout/.gitignore @@ -0,0 +1 @@ +/build diff --git a/TabLayout/build.gradle b/TabLayout/build.gradle new file mode 100644 index 000000000..7838d7412 --- /dev/null +++ b/TabLayout/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion rootProject.ext.android.compileSdkVersion + buildToolsVersion rootProject.ext.android.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.android.minSdkVersion + targetSdkVersion rootProject.ext.android.targetSdkVersion + versionCode rootProject.ext.android.versionCode + versionName rootProject.ext.android.versionName + manifestPlaceholders = rootProject.ext.manifestPlaceholders + + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.7.0' +} + +//apply from: "$gradleHost/master/publish.gradle" \ No newline at end of file diff --git a/TabLayout/consumer-rules.pro b/TabLayout/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/TabLayout/proguard-rules.pro b/TabLayout/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/TabLayout/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/TabLayout/src/main/AndroidManifest.xml b/TabLayout/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e86e891c7 --- /dev/null +++ b/TabLayout/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/AbsDslDrawable.kt b/TabLayout/src/main/java/com/angcyo/tablayout/AbsDslDrawable.kt new file mode 100644 index 000000000..d790825ad --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/AbsDslDrawable.kt @@ -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() + } + + // + + /**附着的[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 + + // + + // + + /**核心方法, 绘制*/ + 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) + } + + // +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslBadgeDrawable.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslBadgeDrawable.kt new file mode 100644 index 000000000..1962d3d8f --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslBadgeDrawable.kt @@ -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) + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslGradientDrawable.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslGradientDrawable.kt new file mode 100644 index 000000000..336c5b9d7 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslGradientDrawable.kt @@ -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) + } + } + + // + + /** + * 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 + } + + // + + // + 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() + } + + // +} + +@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()!! + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslGravity.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslGravity.kt new file mode 100644 index 000000000..66cc1d40e --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslGravity.kt @@ -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 +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslSelector.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslSelector.kt new file mode 100644 index 000000000..955680e86 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslSelector.kt @@ -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 = mutableListOf() + + /** + * 选中的索引列表 + * */ + val selectorIndexList: MutableList = mutableListOf() + get() { + field.clear() + visibleViewList.forEachIndexed { index, view -> + if (view.isSe()) { + field.add(index) + } + } + + return field + } + + /** + * 选中的View列表 + * */ + val selectorViewList: MutableList = mutableListOf() + get() { + field.clear() + visibleViewList.forEachIndexed { index, view -> + if (view.isSe() || index == dslSelectIndex) { + field.add(view) + } + } + return field + } + + //child 点击事件处理 + val _onChildClickListener = View.OnClickListener { + val index = visibleViewList.indexOf(it) + val select = + if (dslSelectorConfig.dslMultiMode || dslSelectorConfig.dslMinSelectLimit < 1) { + !it.isSe() + } else { + true + } + + if (!interceptSelector(index, select, true)) { + selector( + visibleViewList.indexOf(it), + select, + notify = true, + fromUser = true, + forceNotify = it is CompoundButton && dslSelectorConfig.dslMultiMode + ) + } + } + + /**兼容[CompoundButton]*/ + val _onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { buttonView, isChecked -> + buttonView.isChecked = buttonView.isSelected //恢复状态 不做任何处理, 在[OnClickListener]中处理 + /*val index = visibleViewList.indexOf(buttonView) + + if (interceptSelector(index, isChecked, false)) { + //拦截了此操作 + buttonView.isChecked = !isChecked //恢复状态 + } + + val selectorViewList = selectorViewList + val sum = selectorViewList.size + //Limit 过滤 + if (buttonView.isChecked) { + if (sum > dslSelectorConfig.dslMaxSelectLimit) { + //不允许选择 + buttonView.isChecked = false //恢复状态 + } + } else { + //取消选择, 检查是否达到了 limit + if (sum < dslSelectorConfig.dslMinSelectLimit) { + //不允许取消选择 + buttonView.isChecked = true //恢复状态 + } + } + + if (isChecked) { + //已经选中了控件 + } else { + //已经取消了控件 + }*/ + } + + /**当前选中的索引*/ + var dslSelectIndex = -1 + + /**安装*/ + fun install(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector { + dslSelectIndex = -1 + parent = viewGroup + updateVisibleList() + dslSelectorConfig.config() + + updateStyle() + updateClickListener() + + if (dslSelectIndex in 0 until visibleViewList.size) { + selector(dslSelectIndex) + } + + return this + } + + /**更新样式*/ + fun updateStyle() { + visibleViewList.forEachIndexed { index, view -> + val selector = dslSelectIndex == index || view.isSe() + dslSelectorConfig.onStyleItemView(view, index, selector) + } + } + + /**更新child的点击事件*/ + fun updateClickListener() { + parent?.apply { + for (i in 0 until childCount) { + getChildAt(i)?.let { + it.setOnClickListener(_onChildClickListener) + if (it is CompoundButton) { + it.setOnCheckedChangeListener(_onCheckedChangeListener) + } + } + } + } + } + + /**更新可见视图列表*/ + fun updateVisibleList(): List { + visibleViewList.clear() + parent?.apply { + for (i in 0 until childCount) { + getChildAt(i)?.let { + if (it.visibility == View.VISIBLE) { + visibleViewList.add(it) + } + } + } + } + if (dslSelectIndex in visibleViewList.indices) { + if (!visibleViewList[dslSelectIndex].isSe()) { + visibleViewList[dslSelectIndex].setSe(true) + } + } else { + //如果当前选中的索引, 不在可见视图列表中 + dslSelectIndex = -1 + } + return visibleViewList + } + + /** + * 操作单个 + * @param index 操作目标的索引值 + * @param select 选中 or 取消选中 + * @param notify 是否需要通知事件 + * @param forceNotify 是否强制通知事件.child使用[CompoundButton]时, 推荐使用 + * */ + fun selector( + index: Int, + select: Boolean = true, + notify: Boolean = true, + fromUser: Boolean = false, + forceNotify: Boolean = false + ) { + val oldSelectorList = selectorIndexList.toList() + val lastSelectorIndex: Int? = oldSelectorList.lastOrNull() + val reselect = !dslSelectorConfig.dslMultiMode && + oldSelectorList.isNotEmpty() && + oldSelectorList.contains(index) + + var needNotify = _selector(index, select, fromUser) || forceNotify + + if (!oldSelectorList.isChange(selectorIndexList)) { + //选中项, 未改变时不通知 + needNotify = false + } + + if (needNotify || reselect) { + dslSelectIndex = selectorIndexList.lastOrNull() ?: -1 + if (notify) { + notifySelectChange(lastSelectorIndex ?: -1, reselect, fromUser) + } + } + } + + /**选择所有 + * [select] true:选择所有, false:取消所有*/ + fun selectorAll( + select: Boolean = true, + notify: Boolean = true, + fromUser: Boolean = true + ) { + val indexList = visibleViewList.mapIndexedTo(mutableListOf()) { index, _ -> + index + } + selector(indexList, select, notify, fromUser) + } + + /** + * 操作多个 + * @param select 选中 or 取消选中 + * [selector] + * */ + fun selector( + indexList: MutableList, + select: Boolean = true, + notify: Boolean = true, + fromUser: Boolean = false + ) { + val oldSelectorIndexList = selectorIndexList + val lastSelectorIndex: Int? = oldSelectorIndexList.lastOrNull() + + var result = false + + indexList.forEach { + result = _selector(it, select, fromUser) || result + } + + if (result) { + dslSelectIndex = selectorIndexList.lastOrNull() ?: -1 + if (notify) { + notifySelectChange(lastSelectorIndex ?: -1, false, fromUser) + } + } + } + + /**通知事件*/ + fun notifySelectChange(lastSelectorIndex: Int, reselect: Boolean, fromUser: Boolean) { + val indexSelectorList = selectorIndexList + dslSelectorConfig.onSelectViewChange( + visibleViewList.getOrNull(lastSelectorIndex), + selectorViewList, + reselect, + fromUser + ) + dslSelectorConfig.onSelectIndexChange( + lastSelectorIndex, + indexSelectorList, + reselect, + fromUser + ) + } + + /**当前的操作是否被拦截*/ + fun interceptSelector(index: Int, select: Boolean, fromUser: Boolean): Boolean { + val visibleViewList = visibleViewList + if (index !in 0 until visibleViewList.size) { + return true + } + return dslSelectorConfig.onSelectItemView(visibleViewList[index], index, select, fromUser) + } + + /**@return 是否发生过改变*/ + fun _selector(index: Int, select: Boolean, fromUser: Boolean): Boolean { + val visibleViewList = visibleViewList + //超范围过滤 + if (index !in 0 until visibleViewList.size) { + "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, reselect: Boolean, fromUser: Boolean) -> Unit = + { _, _, _, _ -> + + } + + /** + * 选中改变回调 + * [onSelectViewChange] + * @param fromIndex 单选模式下有效, 表示之前选中的[View], 在可见性[child]列表中的索引 + * */ + var onSelectIndexChange: (fromIndex: Int, selectIndexList: List, reselect: Boolean, fromUser: Boolean) -> Unit = + { fromIndex, selectList, reselect, fromUser -> + "选择:[$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) + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabBadge.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabBadge.kt new file mode 100644 index 000000000..0761d1d3a --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabBadge.kt @@ -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 +) \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabBorder.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabBorder.kt new file mode 100644 index 000000000..b1d4d6003 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabBorder.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabDivider.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabDivider.kt new file mode 100644 index 000000000..f0fe41c35 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabDivider.kt @@ -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 + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabHighlight.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabHighlight.kt new file mode 100644 index 000000000..4c21bbd56 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabHighlight.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabIndicator.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabIndicator.kt new file mode 100644 index 000000000..316ced0ad --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabIndicator.kt @@ -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 +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayout.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayout.kt new file mode 100644 index 000000000..d915fd1b2 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayout.kt @@ -0,0 +1,2042 @@ +package com.angcyo.tablayout + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import android.view.* +import android.view.animation.LinearInterpolator +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.OverScroller +import android.widget.TextView +import androidx.core.view.GestureDetectorCompat +import androidx.core.view.GravityCompat +import androidx.core.view.ViewCompat +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * https://github.com/angcyo/DslTabLayout + * Email:angcyo@126.com + * @author angcyo + * @date 2019/11/23 + */ + +open class DslTabLayout( + context: Context, + val attributeSet: AttributeSet? = null +) : ViewGroup(context, attributeSet) { + + /**在未指定[minHeight]的[wrap_content]情况下的高度*/ + var itemDefaultHeight = 40 * dpi + + /**item是否等宽*/ + var itemIsEquWidth = false + + /**item是否支持选择, 只限制点击事件, 不限制滚动事件*/ + var itemEnableSelector = true + + /**当子Item数量在此范围内时,开启等宽,此属性优先级最高 + * [~3] 小于等于3个 + * [3~] 大于等于3个 + * [3~5] 3<= <=5 + * */ + var itemEquWidthCountRange: IntRange? = null + + /**智能判断Item是否等宽. + * 如果所有子项, 未撑满tab时, 则开启等宽模式.此属性会覆盖[itemIsEquWidth]*/ + var itemAutoEquWidth = false + + /**在等宽的情况下, 指定item的宽度, 小于0, 平分*/ + var itemWidth = -3 + + /**是否绘制指示器*/ + var drawIndicator = true + + /**指示器*/ + var tabIndicator: DslTabIndicator = DslTabIndicator(this) + set(value) { + field = value + field.initAttribute(context, attributeSet) + } + + /**指示器动画时长*/ + var tabIndicatorAnimationDuration = 240L + + /**默认选中位置*/ + var tabDefaultIndex = 0 + + /**回调监听器和样式配置器*/ + var tabLayoutConfig: DslTabLayoutConfig? = null + set(value) { + field = value + + field?.initAttribute(context, attributeSet) + } + + /**边框绘制*/ + var tabBorder: DslTabBorder? = null + set(value) { + field = value + field?.callback = this + field?.initAttribute(context, attributeSet) + } + var drawBorder = false + + /**垂直分割线*/ + var tabDivider: DslTabDivider? = null + set(value) { + field = value + field?.callback = this + field?.initAttribute(context, attributeSet) + } + var drawDivider = false + + /**未读数角标*/ + var tabBadge: DslTabBadge? = null + set(value) { + field = value + field?.callback = this + field?.initAttribute(context, attributeSet) + } + var drawBadge = false + + /**快速角标配置项, 方便使用者*/ + val tabBadgeConfigMap = mutableMapOf() + + /**角标绘制配置*/ + var onTabBadgeConfig: (child: View, tabBadge: DslTabBadge, index: Int) -> TabBadgeConfig? = + { _, tabBadge, index -> + val badgeConfig = getBadgeConfig(index) + if (!isInEditMode) { + tabBadge.updateBadgeConfig(badgeConfig) + } + badgeConfig + } + + /**是否绘制突出*/ + var drawHighlight = false + + /**选中突出提示*/ + var tabHighlight: DslTabHighlight? = null + set(value) { + field = value + field?.callback = this + field?.initAttribute(context, attributeSet) + } + + /**如果使用了高凸模式. 请使用这个属性设置背景色*/ + var tabConvexBackgroundDrawable: Drawable? = null + + /**是否激活滑动选择模式*/ + var tabEnableSelectorMode = false + + /**布局的方向*/ + var orientation: Int = LinearLayout.HORIZONTAL + + /**布局时, 滚动到居中是否需要动画*/ + var layoutScrollAnim: Boolean = false + + /**滚动动画的时长*/ + var scrollAnimDuration = 250 + + // + + //fling 速率阈值 + var _minFlingVelocity = 0 + var _maxFlingVelocity = 0 + + //scroll 阈值 + var _touchSlop = 0 + + //临时变量 + val _tempRect = Rect() + + //childView选择器 + val dslSelector: DslSelector by lazy { + DslSelector().install(this) { + onStyleItemView = { itemView, index, select -> + tabLayoutConfig?.onStyleItemView?.invoke(itemView, index, select) + } + onSelectItemView = { itemView, index, select, fromUser -> + tabLayoutConfig?.onSelectItemView?.invoke(itemView, index, select, fromUser) + ?: false + } + onSelectViewChange = { fromView, selectViewList, reselect, fromUser -> + tabLayoutConfig?.onSelectViewChange?.invoke( + fromView, + selectViewList, + reselect, + fromUser + ) + } + onSelectIndexChange = { fromIndex, selectList, reselect, fromUser -> + if (tabLayoutConfig == null) { + "选择:[$fromIndex]->${selectList} reselect:$reselect fromUser:$fromUser".logi() + } + + val toIndex = selectList.lastOrNull() ?: -1 + _animateToItem(fromIndex, toIndex) + + _scrollToTarget(toIndex, tabIndicator.indicatorAnim) + postInvalidate() + + //如果设置[tabLayoutConfig?.onSelectIndexChange], 那么会覆盖[_viewPagerDelegate]的操作. + tabLayoutConfig?.onSelectIndexChange?.invoke( + fromIndex, + selectList, + reselect, + fromUser + ) ?: _viewPagerDelegate?.onSetCurrentItem(fromIndex, toIndex, reselect, fromUser) + } + } + } + + init { + val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout) + itemIsEquWidth = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_item_is_equ_width, itemIsEquWidth) + val maxEquWidthCount = + typedArray.getInt(R.styleable.DslTabLayout_tab_item_equ_width_count, -1) + if (maxEquWidthCount >= 0) { + itemEquWidthCountRange = IntRange(maxEquWidthCount, Int.MAX_VALUE) + } + if (typedArray.hasValue(R.styleable.DslTabLayout_tab_item_equ_width_count_range)) { + val equWidthCountRangeString = + typedArray.getString(R.styleable.DslTabLayout_tab_item_equ_width_count_range) + if (equWidthCountRangeString.isNullOrBlank()) { + itemEquWidthCountRange = null + } else { + val rangeList = equWidthCountRangeString.split("~") + if (rangeList.size() >= 2) { + val min = rangeList.getOrNull(0)?.toIntOrNull() ?: 0 + val max = rangeList.getOrNull(1)?.toIntOrNull() ?: Int.MAX_VALUE + itemEquWidthCountRange = IntRange(min, max) + } else { + val min = rangeList.getOrNull(0)?.toIntOrNull() ?: Int.MAX_VALUE + itemEquWidthCountRange = IntRange(min, Int.MAX_VALUE) + } + } + } + itemAutoEquWidth = typedArray.getBoolean( + R.styleable.DslTabLayout_tab_item_auto_equ_width, + itemAutoEquWidth + ) + itemWidth = + typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_item_width, itemWidth) + itemDefaultHeight = typedArray.getDimensionPixelOffset( + R.styleable.DslTabLayout_tab_item_default_height, + itemDefaultHeight + ) + tabDefaultIndex = + typedArray.getInt(R.styleable.DslTabLayout_tab_default_index, tabDefaultIndex) + + drawIndicator = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_draw_indicator, drawIndicator) + drawDivider = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_draw_divider, drawDivider) + drawBorder = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_draw_border, drawBorder) + drawBadge = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_draw_badge, drawBadge) + drawHighlight = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_draw_highlight, drawHighlight) + + tabEnableSelectorMode = + typedArray.getBoolean( + R.styleable.DslTabLayout_tab_enable_selector_mode, + tabEnableSelectorMode + ) + + tabConvexBackgroundDrawable = + typedArray.getDrawable(R.styleable.DslTabLayout_tab_convex_background) + + orientation = typedArray.getInt(R.styleable.DslTabLayout_tab_orientation, orientation) + + layoutScrollAnim = + typedArray.getBoolean(R.styleable.DslTabLayout_tab_layout_scroll_anim, layoutScrollAnim) + scrollAnimDuration = + typedArray.getInt(R.styleable.DslTabLayout_tab_scroll_anim_duration, scrollAnimDuration) + + //preview + if (isInEditMode) { + val layoutId = + typedArray.getResourceId(R.styleable.DslTabLayout_tab_preview_item_layout_id, -1) + val layoutCount = + typedArray.getInt(R.styleable.DslTabLayout_tab_preview_item_count, 3) + if (layoutId != -1) { + for (i in 0 until layoutCount) { + inflate(layoutId, true).let { + if (it is TextView) { + if (it.text.isNullOrEmpty()) { + it.text = "Item $i" + } else { + it.text = "${it.text}/$i" + } + } + } + } + } + } + + typedArray.recycle() + + val vc = ViewConfiguration.get(context) + _minFlingVelocity = vc.scaledMinimumFlingVelocity + _maxFlingVelocity = vc.scaledMaximumFlingVelocity + //_touchSlop = vc.scaledTouchSlop + + if (drawIndicator) { + //直接初始化的变量, 不会触发set方法. + tabIndicator.initAttribute(context, attributeSet) + } + + if (drawBorder) { + tabBorder = DslTabBorder() + } + if (drawDivider) { + tabDivider = DslTabDivider() + } + if (drawBadge) { + tabBadge = DslTabBadge() + } + if (drawHighlight) { + tabHighlight = DslTabHighlight(this) + } + + //样式配置器 + tabLayoutConfig = DslTabLayoutConfig(this) + + //开启绘制 + setWillNotDraw(false) + } + + // + + // + + /**当前选中item的索引*/ + val currentItemIndex: Int + get() = dslSelector.dslSelectIndex + + /**当前选中的itemView*/ + val currentItemView: View? + get() = dslSelector.visibleViewList.getOrNull(currentItemIndex) + + /**设置tab的位置*/ + fun setCurrentItem(index: Int, notify: Boolean = true, fromUser: Boolean = false) { + if (currentItemIndex == index) { + _scrollToTarget(index, tabIndicator.indicatorAnim) + return + } + dslSelector.selector(index, true, notify, fromUser) + } + + /**关联[ViewPagerDelegate]*/ + fun setupViewPager(viewPagerDelegate: ViewPagerDelegate) { + _viewPagerDelegate = viewPagerDelegate + } + + /**配置一个新的[DslTabLayoutConfig]给[DslTabLayout]*/ + fun setTabLayoutConfig( + config: DslTabLayoutConfig = DslTabLayoutConfig(this), + doIt: DslTabLayoutConfig.() -> Unit = {} + ) { + tabLayoutConfig = config + configTabLayoutConfig(doIt) + } + + /**配置[DslTabLayoutConfig]*/ + fun configTabLayoutConfig(config: DslTabLayoutConfig.() -> Unit = {}) { + if (tabLayoutConfig == null) { + tabLayoutConfig = DslTabLayoutConfig(this) + } + tabLayoutConfig?.config() + dslSelector.updateStyle() + } + + /**观察index的改变回调*/ + fun observeIndexChange( + config: DslTabLayoutConfig.() -> Unit = {}, + action: (fromIndex: Int, toIndex: Int, reselect: Boolean, fromUser: Boolean) -> Unit + ) { + configTabLayoutConfig { + config() + onSelectIndexChange = { fromIndex, selectIndexList, reselect, fromUser -> + action(fromIndex, selectIndexList.firstOrNull() ?: -1, reselect, fromUser) + } + } + } + + fun getBadgeConfig(index: Int): TabBadgeConfig { + return tabBadgeConfigMap.getOrElse(index) { + tabBadge?.defaultBadgeConfig?.copy() ?: TabBadgeConfig() + } + } + + fun updateTabBadge(index: Int, badgeText: String?) { + updateTabBadge(index) { + this.badgeText = badgeText + } + } + + /**更新角标*/ + fun updateTabBadge(index: Int, config: TabBadgeConfig.() -> Unit) { + val badgeConfig = getBadgeConfig(index) + tabBadgeConfigMap[index] = badgeConfig + badgeConfig.config() + postInvalidate() + } + + // + + // + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + } + + override fun onFinishInflate() { + super.onFinishInflate() + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + updateTabLayout() + } + + override fun onViewRemoved(child: View?) { + super.onViewRemoved(child) + updateTabLayout() + } + + open fun updateTabLayout() { + dslSelector.updateVisibleList() + dslSelector.updateStyle() + dslSelector.updateClickListener() + } + + override fun draw(canvas: Canvas) { + //Log.e("angcyo", "...draw...") + + if (drawIndicator) { + tabIndicator.setBounds(0, 0, measuredWidth, measuredHeight) + } + + //自定义的背景 + tabConvexBackgroundDrawable?.apply { + if (isHorizontal()) { + setBounds(0, _maxConvexHeight, right - left, bottom - top) + } else { + setBounds(0, 0, measuredWidth - _maxConvexHeight, bottom - top) + } + + if (scrollX or scrollY == 0) { + draw(canvas) + } else { + canvas.holdLocation { + draw(canvas) + } + } + } + + super.draw(canvas) + + //突出显示 + if (drawHighlight) { + tabHighlight?.draw(canvas) + } + + val visibleChildCount = dslSelector.visibleViewList.size + + //绘制在child的上面 + if (drawDivider) { + if (isHorizontal()) { + if (isLayoutRtl) { + var right = 0 + tabDivider?.apply { + val top = paddingTop + dividerMarginTop + val bottom = measuredHeight - paddingBottom - dividerMarginBottom + dslSelector.visibleViewList.forEachIndexed { index, view -> + + if (haveBeforeDivider(index, visibleChildCount)) { + right = view.right + dividerMarginLeft + dividerWidth + setBounds(right - dividerWidth, top, right, bottom) + draw(canvas) + } + + if (haveAfterDivider(index, visibleChildCount)) { + right = view.right - view.measuredWidth - dividerMarginRight + setBounds(right - dividerWidth, top, right, bottom) + draw(canvas) + } + + } + } + } else { + var left = 0 + tabDivider?.apply { + val top = paddingTop + dividerMarginTop + val bottom = measuredHeight - paddingBottom - dividerMarginBottom + dslSelector.visibleViewList.forEachIndexed { index, view -> + + if (haveBeforeDivider(index, visibleChildCount)) { + left = view.left - dividerMarginRight - dividerWidth + setBounds(left, top, left + dividerWidth, bottom) + draw(canvas) + } + + if (haveAfterDivider(index, visibleChildCount)) { + left = view.right + dividerMarginLeft + setBounds(left, top, left + dividerWidth, bottom) + draw(canvas) + } + + } + } + } + } else { + var top = 0 + tabDivider?.apply { + val left = paddingStart + dividerMarginLeft + val right = measuredWidth - paddingEnd - dividerMarginRight + dslSelector.visibleViewList.forEachIndexed { index, view -> + + if (haveBeforeDivider(index, visibleChildCount)) { + top = view.top - dividerMarginBottom - dividerHeight + setBounds(left, top, right, top + dividerHeight) + draw(canvas) + } + + if (haveAfterDivider(index, visibleChildCount)) { + top = view.bottom + dividerMarginTop + setBounds(left, top, right, top + dividerHeight) + draw(canvas) + } + } + } + } + } + if (drawBorder) { + //边框不跟随滚动 + canvas.holdLocation { + tabBorder?.draw(canvas) + } + } + if (drawIndicator && tabIndicator.indicatorStyle.have(DslTabIndicator.INDICATOR_STYLE_FOREGROUND)) { + //前景显示 + tabIndicator.draw(canvas) + } + if (drawBadge) { + tabBadge?.apply { + dslSelector.visibleViewList.forEachIndexed { index, child -> + val badgeConfig = onTabBadgeConfig(child, this, index) + + var left: Int + var top: Int + var right: Int + var bottom: Int + + var anchorView: View = child + + if (badgeConfig != null && badgeConfig.badgeAnchorChildIndex >= 0) { + anchorView = + child.getChildOrNull(badgeConfig.badgeAnchorChildIndex) ?: child + + anchorView.getLocationInParent(this@DslTabLayout, _tempRect) + + left = _tempRect.left + top = _tempRect.top + right = _tempRect.right + bottom = _tempRect.bottom + } else { + left = anchorView.left + top = anchorView.top + right = anchorView.right + bottom = anchorView.bottom + } + + if (badgeConfig != null && badgeConfig.badgeIgnoreChildPadding) { + left += anchorView.paddingStart + top += anchorView.paddingTop + right -= anchorView.paddingEnd + bottom -= anchorView.paddingBottom + } + + setBounds(left, top, right, bottom) + + updateOriginDrawable() + + if (isInEditMode) { + badgeText = if (index == visibleChildCount - 1) { + //预览中, 强制最后一个角标为圆点类型, 方便查看预览. + "" + } else { + xmlBadgeText + } + } + + draw(canvas) + } + } + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (drawBorder) { + //边框不跟随滚动 + canvas.holdLocation { + tabBorder?.drawBorderBackground(canvas) + } + } + + //绘制在child的后面 + if (drawIndicator && !tabIndicator.indicatorStyle.have(DslTabIndicator.INDICATOR_STYLE_FOREGROUND)) { + //背景绘制 + tabIndicator.draw(canvas) + } + } + + /**保持位置不变*/ + fun Canvas.holdLocation(action: () -> Unit) { + translate(scrollX.toFloat(), scrollY.toFloat()) + action() + translate(-scrollX.toFloat(), -scrollY.toFloat()) + } + + override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { + return super.drawChild(canvas, child, drawingTime) + } + + override fun verifyDrawable(who: Drawable): Boolean { + return super.verifyDrawable(who) || + who == tabIndicator /*|| + who == tabBorder || + who == tabDivider || + who == tabConvexBackgroundDrawable*/ /*|| + who == tabBadge 防止循环绘制*/ + } + + // + + // + + //所有child的总宽度, 不包含parent的padding + var _childAllWidthSum = 0 + + //最大的凸起高度 + var _maxConvexHeight = 0 + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + + if (dslSelector.dslSelectIndex < 0) { + //还没有选中 + setCurrentItem(tabDefaultIndex) + } + + if (isHorizontal()) { + measureHorizontal(widthMeasureSpec, heightMeasureSpec) + } else { + measureVertical(widthMeasureSpec, heightMeasureSpec) + } + } + + fun measureHorizontal(widthMeasureSpec: Int, heightMeasureSpec: Int) { + dslSelector.updateVisibleList() + + val visibleChildList = dslSelector.visibleViewList + val visibleChildCount = visibleChildList.size + + //控制最小大小 + val tabMinHeight = if (suggestedMinimumHeight > 0) { + suggestedMinimumHeight + } else { + itemDefaultHeight + } + + if (visibleChildCount == 0) { + setMeasuredDimension( + getDefaultSize(suggestedMinimumWidth, widthMeasureSpec), + getDefaultSize(tabMinHeight, heightMeasureSpec) + ) + return + } + + //super.onMeasure(widthMeasureSpec, heightMeasureSpec) + var widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + var heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + + _maxConvexHeight = 0 + + var childWidthSpec: Int = -1 + + //记录child最大的height, 用来实现tabLayout wrap_content, 包括突出的大小 + var childMaxHeight = tabMinHeight //child最大的高度 + + if (heightMode == MeasureSpec.UNSPECIFIED) { + if (heightSize == 0) { + heightSize = Int.MAX_VALUE + } + } + + if (widthMode == MeasureSpec.UNSPECIFIED) { + if (widthSize == 0) { + widthSize = Int.MAX_VALUE + } + } + + //分割线需要排除的宽度 + val dividerWidthExclude = + if (drawDivider) tabDivider?.run { dividerWidth + dividerMarginLeft + dividerMarginRight } + ?: 0 else 0 + + //智能等宽判断 + if (itemAutoEquWidth) { + var childMaxWidth = 0 //所有child宽度总和 + visibleChildList.forEachIndexed { index, child -> + val lp: LayoutParams = child.layoutParams as LayoutParams + measureChild(child, widthMeasureSpec, heightMeasureSpec) + childMaxWidth += lp.marginStart + lp.marginEnd + child.measuredWidth + + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true) { + childMaxWidth += dividerWidthExclude + } + if (tabDivider?.haveAfterDivider(index, visibleChildList.size) == true) { + childMaxWidth += dividerWidthExclude + } + } + } + + itemIsEquWidth = childMaxWidth <= widthSize + } + + itemEquWidthCountRange?.let { + itemIsEquWidth = it.contains(visibleChildCount) + } + + //等宽时, child宽度的测量模式 + val childEquWidthSpec = if (itemIsEquWidth) { + exactlyMeasure( + if (itemWidth > 0) { + itemWidth + } else { + var excludeWidth = paddingStart + paddingEnd + visibleChildList.forEachIndexed { index, child -> + if (drawDivider) { + if (tabDivider?.haveBeforeDivider( + index, + visibleChildList.size + ) == true + ) { + excludeWidth += dividerWidthExclude + } + if (tabDivider?.haveAfterDivider( + index, + visibleChildList.size + ) == true + ) { + excludeWidth += dividerWidthExclude + } + } + val lp = child.layoutParams as LayoutParams + excludeWidth += lp.marginStart + lp.marginEnd + } + (widthSize - excludeWidth) / visibleChildCount + } + ) + } else { + -1 + } + + //...end + + _childAllWidthSum = 0 + + //没有设置weight属性的child宽度总和, 用于计算剩余空间 + var allChildUsedWidth = 0 + + fun measureChild(childView: View, heightSpec: Int? = null) { + val lp = childView.layoutParams as LayoutParams + + //child高度测量模式 + var childHeightSpec: Int = -1 + + val widthHeight = calcLayoutWidthHeight( + lp.layoutWidth, lp.layoutHeight, + widthSize, heightSize, 0, 0 + ) + + if (heightMode == MeasureSpec.EXACTLY) { + //固定高度 + childHeightSpec = + exactlyMeasure(heightSize - paddingTop - paddingBottom - lp.topMargin - lp.bottomMargin) + } else { + if (widthHeight[1] > 0) { + heightSize = widthHeight[1] + childHeightSpec = exactlyMeasure(heightSize) + heightSize += paddingTop + paddingBottom + } else { + childHeightSpec = if (lp.height == ViewGroup.LayoutParams.MATCH_PARENT) { + exactlyMeasure(tabMinHeight) + } else { + atmostMeasure(Int.MAX_VALUE) + } + } + } + + val childConvexHeight = lp.layoutConvexHeight + + //...end + + //计算宽度测量模式 + childWidthSpec //no op + + if (heightSpec != null) { + childView.measure(childWidthSpec, heightSpec) + } else { + childView.measure(childWidthSpec, childHeightSpec) + } + if (childConvexHeight > 0) { + _maxConvexHeight = max(_maxConvexHeight, childConvexHeight) + //需要凸起 + val spec = exactlyMeasure(childView.measuredHeight + childConvexHeight) + childView.measure(childWidthSpec, spec) + } + childMaxHeight = max(childMaxHeight, childView.measuredHeight) + } + + visibleChildList.forEachIndexed { index, childView -> + val lp = childView.layoutParams as LayoutParams + var childUsedWidth = 0 + if (lp.weight < 0) { + val widthHeight = calcLayoutWidthHeight( + lp.layoutWidth, lp.layoutHeight, + widthSize, heightSize, 0, 0 + ) + + //计算宽度测量模式 + childWidthSpec = when { + itemIsEquWidth -> childEquWidthSpec + widthHeight[0] > 0 -> exactlyMeasure(widthHeight[0]) + lp.width == ViewGroup.LayoutParams.MATCH_PARENT -> exactlyMeasure(widthSize - paddingStart - paddingEnd) + lp.width > 0 -> exactlyMeasure(lp.width) + else -> atmostMeasure(widthSize - paddingStart - paddingEnd) + } + + measureChild(childView) + + childUsedWidth = childView.measuredWidth + lp.marginStart + lp.marginEnd + } else { + childUsedWidth = lp.marginStart + lp.marginEnd + } + + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true) { + childUsedWidth += dividerWidthExclude + } + if (tabDivider?.haveAfterDivider(index, visibleChildList.size) == true) { + childUsedWidth += dividerWidthExclude + } + } + + childMaxHeight = max(childMaxHeight, childView.measuredHeight) + allChildUsedWidth += childUsedWidth + _childAllWidthSum += childUsedWidth + } + + //剩余空间 + val spaceSize = widthSize - allChildUsedWidth + + //计算weight + visibleChildList.forEach { childView -> + val lp = childView.layoutParams as LayoutParams + if (lp.weight > 0) { + val widthHeight = calcLayoutWidthHeight( + lp.layoutWidth, lp.layoutHeight, + widthSize, heightSize, 0, 0 + ) + + //计算宽度测量模式 + childWidthSpec = when { + itemIsEquWidth -> childEquWidthSpec + spaceSize > 0 -> exactlyMeasure(spaceSize * lp.weight) + widthHeight[0] > 0 -> exactlyMeasure(allChildUsedWidth) + lp.width == ViewGroup.LayoutParams.MATCH_PARENT -> exactlyMeasure(widthSize - paddingStart - paddingEnd) + lp.width > 0 -> exactlyMeasure(lp.width) + else -> atmostMeasure(widthSize - paddingStart - paddingEnd) + } + + measureChild(childView) + + childMaxHeight = max(childMaxHeight, childView.measuredHeight) + + //上面已经处理了分割线和margin的距离了 + _childAllWidthSum += childView.measuredWidth + } + } + //...end + + if (heightMode == MeasureSpec.AT_MOST) { + //wrap_content 情况下, 重新测量所有子view + val childHeightSpec = exactlyMeasure( + max( + childMaxHeight - _maxConvexHeight, + suggestedMinimumHeight - paddingTop - paddingBottom + ) + ) + visibleChildList.forEach { childView -> + measureChild(childView, childHeightSpec) + } + } + + if (widthMode != MeasureSpec.EXACTLY) { + widthSize = min(_childAllWidthSum + paddingStart + paddingEnd, widthSize) + } + + if (visibleChildList.isEmpty()) { + heightSize = if (suggestedMinimumHeight > 0) { + suggestedMinimumHeight + } else { + itemDefaultHeight + } + } else if (heightMode != MeasureSpec.EXACTLY) { + heightSize = max( + childMaxHeight - _maxConvexHeight + paddingTop + paddingBottom, + suggestedMinimumHeight + ) + } + + setMeasuredDimension(widthSize, heightSize + _maxConvexHeight) + } + + fun measureVertical(widthMeasureSpec: Int, heightMeasureSpec: Int) { + dslSelector.updateVisibleList() + + val visibleChildList = dslSelector.visibleViewList + val visibleChildCount = visibleChildList.size + + if (visibleChildCount == 0) { + setMeasuredDimension( + getDefaultSize( + if (suggestedMinimumHeight > 0) { + suggestedMinimumHeight + } else { + itemDefaultHeight + }, widthMeasureSpec + ), + getDefaultSize(suggestedMinimumHeight, heightMeasureSpec) + ) + return + } + + //super.onMeasure(widthMeasureSpec, heightMeasureSpec) + var widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + var heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + + _maxConvexHeight = 0 + + //child高度测量模式 + var childHeightSpec: Int = -1 + var childWidthSpec: Int = -1 + + if (heightMode == MeasureSpec.UNSPECIFIED) { + if (heightSize == 0) { + heightSize = Int.MAX_VALUE + } + } + + if (widthMode == MeasureSpec.EXACTLY) { + //固定宽度 + childWidthSpec = exactlyMeasure(widthSize - paddingStart - paddingEnd) + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + if (widthSize == 0) { + widthSize = Int.MAX_VALUE + } + } + + //分割线需要排除的宽度 + val dividerHeightExclude = + if (drawDivider) tabDivider?.run { dividerHeight + dividerMarginTop + dividerMarginBottom } + ?: 0 else 0 + + //智能等宽判断 + if (itemAutoEquWidth) { + var childMaxHeight = 0 //所有child高度总和 + visibleChildList.forEachIndexed { index, child -> + val lp: LayoutParams = child.layoutParams as LayoutParams + measureChild(child, widthMeasureSpec, heightMeasureSpec) + childMaxHeight += lp.topMargin + lp.bottomMargin + child.measuredHeight + + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true) { + childMaxHeight += dividerHeightExclude + } + if (tabDivider?.haveAfterDivider(index, visibleChildList.size) == true) { + childMaxHeight += dividerHeightExclude + } + } + } + + itemIsEquWidth = childMaxHeight <= heightSize + } + + itemEquWidthCountRange?.let { + itemIsEquWidth = it.contains(visibleChildCount) + } + + //等宽时, child高度的测量模式 + val childEquHeightSpec = if (itemIsEquWidth) { + exactlyMeasure( + if (itemWidth > 0) { + itemWidth + } else { + var excludeHeight = paddingTop + paddingBottom + visibleChildList.forEachIndexed { index, child -> + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true + ) { + excludeHeight += dividerHeightExclude + } + if (tabDivider?.haveAfterDivider(index, visibleChildList.size) == true + ) { + excludeHeight += dividerHeightExclude + } + } + val lp = child.layoutParams as LayoutParams + excludeHeight += lp.topMargin + lp.bottomMargin + } + (heightSize - excludeHeight) / visibleChildCount + } + ) + } else { + -1 + } + + //...end + + _childAllWidthSum = 0 + + var wrapContentWidth = false + + //没有设置weight属性的child宽度总和, 用于计算剩余空间 + var allChildUsedHeight = 0 + + fun measureChild(childView: View) { + val lp = childView.layoutParams as LayoutParams + + //纵向布局, 不支持横向margin支持 + lp.marginStart = 0 + lp.marginEnd = 0 + + val childConvexHeight = lp.layoutConvexHeight + _maxConvexHeight = max(_maxConvexHeight, childConvexHeight) + + val widthHeight = calcLayoutWidthHeight( + lp.layoutWidth, lp.layoutHeight, + widthSize, heightSize, 0, 0 + ) + + //计算宽度测量模式 + wrapContentWidth = false + if (childWidthSpec == -1) { + if (widthHeight[0] > 0) { + widthSize = widthHeight[0] + childWidthSpec = exactlyMeasure(widthSize) + widthSize += paddingStart + paddingEnd + } + } + + if (childWidthSpec == -1) { + if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { + + widthSize = if (suggestedMinimumWidth > 0) { + suggestedMinimumWidth + } else { + itemDefaultHeight + } + + childWidthSpec = exactlyMeasure(widthSize) + + widthSize += paddingStart + paddingEnd + } else { + childWidthSpec = atmostMeasure(widthSize) + wrapContentWidth = true + } + } + //...end + + //计算高度测量模式 + childHeightSpec //no op + + if (childConvexHeight > 0) { + //需要凸起 + val childConvexWidthSpec = MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(childWidthSpec) + childConvexHeight, + MeasureSpec.getMode(childWidthSpec) + ) + childView.measure(childConvexWidthSpec, childHeightSpec) + } else { + childView.measure(childWidthSpec, childHeightSpec) + } + + if (wrapContentWidth) { + widthSize = childView.measuredWidth + childWidthSpec = exactlyMeasure(widthSize) + widthSize += paddingStart + paddingEnd + } + } + + visibleChildList.forEachIndexed { index, childView -> + val lp = childView.layoutParams as LayoutParams + var childUsedHeight = 0 + if (lp.weight < 0) { + val widthHeight = calcLayoutWidthHeight( + lp.layoutWidth, lp.layoutHeight, + widthSize, heightSize, 0, 0 + ) + + //计算高度测量模式 + childHeightSpec = when { + itemIsEquWidth -> childEquHeightSpec + widthHeight[1] > 0 -> exactlyMeasure(widthHeight[1]) + lp.height == ViewGroup.LayoutParams.MATCH_PARENT -> exactlyMeasure(heightSize - paddingTop - paddingBottom) + lp.height > 0 -> exactlyMeasure(lp.height) + else -> atmostMeasure(heightSize - paddingTop - paddingBottom) + } + + measureChild(childView) + + childUsedHeight = childView.measuredHeight + lp.topMargin + lp.bottomMargin + } else { + childUsedHeight = lp.topMargin + lp.bottomMargin + } + + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true) { + childUsedHeight += dividerHeightExclude + } + if (tabDivider?.haveAfterDivider(index, visibleChildList.size) == true) { + childUsedHeight += dividerHeightExclude + } + } + + allChildUsedHeight += childUsedHeight + _childAllWidthSum += childUsedHeight + } + + //剩余空间 + val spaceSize = heightSize - allChildUsedHeight + + //计算weight + visibleChildList.forEach { childView -> + val lp = childView.layoutParams as LayoutParams + if (lp.weight > 0) { + val widthHeight = calcLayoutWidthHeight( + lp.layoutWidth, lp.layoutHeight, + widthSize, heightSize, 0, 0 + ) + + //计算高度测量模式 + childHeightSpec = when { + itemIsEquWidth -> childEquHeightSpec + spaceSize > 0 -> exactlyMeasure(spaceSize * lp.weight) + widthHeight[1] > 0 -> exactlyMeasure(allChildUsedHeight) + lp.height == ViewGroup.LayoutParams.MATCH_PARENT -> exactlyMeasure(heightSize - paddingTop - paddingBottom) + lp.height > 0 -> exactlyMeasure(lp.height) + else -> atmostMeasure(heightSize - paddingTop - paddingBottom) + } + + measureChild(childView) + + //上面已经处理了分割线和margin的距离了 + _childAllWidthSum += childView.measuredHeight + } + } + //...end + + if (heightMode != MeasureSpec.EXACTLY) { + heightSize = min(_childAllWidthSum + paddingTop + paddingBottom, heightSize) + } + + if (visibleChildList.isEmpty()) { + widthSize = if (suggestedMinimumWidth > 0) { + suggestedMinimumWidth + } else { + itemDefaultHeight + } + } + + setMeasuredDimension(widthSize + _maxConvexHeight, heightSize) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + if (isHorizontal()) { + layoutHorizontal(changed, l, t, r, b) + } else { + layoutVertical(changed, l, t, r, b) + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + //check + restoreScroll() + + if (dslSelector.dslSelectIndex < 0) { + //还没有选中 + setCurrentItem(tabDefaultIndex) + } else { + if (_overScroller.isFinished) { + _scrollToTarget(dslSelector.dslSelectIndex, layoutScrollAnim) + } + } + } + + val isLayoutRtl: Boolean + get() = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL + + var _layoutDirection: Int = -1 + + //API 17 + override fun onRtlPropertiesChanged(layoutDirection: Int) { + super.onRtlPropertiesChanged(layoutDirection) + + if (layoutDirection != _layoutDirection) { + _layoutDirection = layoutDirection + if (orientation == LinearLayout.HORIZONTAL) { + updateTabLayout() //更新样式 + requestLayout() //重新布局 + } + } + } + + fun layoutHorizontal(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + val isRtl = isLayoutRtl + + var left = paddingStart + var right = measuredWidth - paddingEnd + + var childBottom = measuredHeight - paddingBottom + + val dividerExclude = if (drawDivider) tabDivider?.run { + dividerWidth + dividerMarginLeft + dividerMarginRight + } ?: 0 else 0 + + val visibleChildList = dslSelector.visibleViewList + visibleChildList.forEachIndexed { index, childView -> + + val lp = childView.layoutParams as LayoutParams + val verticalGravity = lp.gravity and Gravity.VERTICAL_GRAVITY_MASK + + if (isRtl) { + right -= lp.marginEnd + } else { + left += lp.marginStart + } + + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true) { + + if (isRtl) { + right -= dividerExclude + } else { + left += dividerExclude + } + } + } + + childBottom = when (verticalGravity) { + Gravity.CENTER_VERTICAL -> measuredHeight - paddingBottom - + ((measuredHeight - paddingTop - paddingBottom - _maxConvexHeight) / 2 - + childView.measuredHeight / 2) + + Gravity.BOTTOM -> measuredHeight - paddingBottom + else -> paddingTop + lp.topMargin + childView.measuredHeight + } + + if (isRtl) { + childView.layout( + right - childView.measuredWidth, + childBottom - childView.measuredHeight, + right, + childBottom + ) + right -= childView.measuredWidth + lp.marginStart + } else { + childView.layout( + left, + childBottom - childView.measuredHeight, + left + childView.measuredWidth, + childBottom + ) + left += childView.measuredWidth + lp.marginEnd + } + } + + //check + restoreScroll() + + if (dslSelector.dslSelectIndex < 0) { + //还没有选中 + setCurrentItem(tabDefaultIndex) + } else { + if (_overScroller.isFinished) { + _scrollToTarget(dslSelector.dslSelectIndex, layoutScrollAnim) + } + } + } + + fun layoutVertical(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + var top = paddingTop + var childLeft = paddingStart + + val dividerExclude = + if (drawDivider) tabDivider?.run { dividerHeight + dividerMarginTop + dividerMarginBottom } + ?: 0 else 0 + + val visibleChildList = dslSelector.visibleViewList + visibleChildList.forEachIndexed { index, childView -> + + val lp = childView.layoutParams as LayoutParams + val layoutDirection = 0 + val absoluteGravity = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection) + val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK + + top += lp.topMargin + + if (drawDivider) { + if (tabDivider?.haveBeforeDivider(index, visibleChildList.size) == true) { + top += dividerExclude + } + } + + childLeft = when (horizontalGravity) { + Gravity.CENTER_HORIZONTAL -> paddingStart + ((measuredWidth - paddingStart - paddingEnd - _maxConvexHeight) / 2 - + childView.measuredWidth / 2) + + Gravity.RIGHT -> measuredWidth - paddingRight - childView.measuredWidth - lp.rightMargin + else -> paddingLeft + lp.leftMargin + } + + /*默认水平居中显示*/ + childView.layout( + childLeft, + top, + childLeft + childView.measuredWidth, + top + childView.measuredHeight + ) + + top += childView.measuredHeight + lp.bottomMargin + } + } + + /**是否是横向布局*/ + fun isHorizontal() = orientation.isHorizontal() + + // + + // + + override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams { + return LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + } + + override fun generateLayoutParams(attrs: AttributeSet?): ViewGroup.LayoutParams { + return LayoutParams(context, attrs) + } + + override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams { + return p?.run { LayoutParams(p) } ?: generateDefaultLayoutParams() + } + + class LayoutParams : FrameLayout.LayoutParams { + + /** + * 支持格式0.3pw 0.5ph, 表示[parent]的多少倍数 + * */ + var layoutWidth: String? = null + var layoutHeight: String? = null + + /**凸出的高度*/ + var layoutConvexHeight: Int = 0 + + /** + * 宽高[WRAP_CONTENT]时, 内容view的定位索引 + * [TabIndicator.indicatorContentIndex] + * */ + var indicatorContentIndex = -1 + var indicatorContentId = View.NO_ID + + /**[com.angcyo.tablayout.DslTabLayoutConfig.getOnGetTextStyleView]*/ + var contentTextViewIndex = -1 + + /**[com.angcyo.tablayout.DslTabLayoutConfig.getTabTextViewId]*/ + var contentTextViewId = View.NO_ID + + /**[com.angcyo.tablayout.DslTabLayoutConfig.getOnGetIcoStyleView]*/ + var contentIconViewIndex = -1 + + /**[com.angcyo.tablayout.DslTabLayoutConfig.getTabIconViewId]*/ + var contentIconViewId = View.NO_ID + + /** + * 剩余空间占比, 1f表示占满剩余空间, 0.5f表示使用剩余空间的0.5倍 + * [android.widget.LinearLayout.LayoutParams.weight]*/ + var weight: Float = -1f + + /**突出需要绘制的Drawable + * [com.angcyo.tablayout.DslTabHighlight.highlightDrawable]*/ + var highlightDrawable: Drawable? = null + + constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { + val a = c.obtainStyledAttributes(attrs, R.styleable.DslTabLayout_Layout) + layoutWidth = a.getString(R.styleable.DslTabLayout_Layout_layout_tab_width) + layoutHeight = a.getString(R.styleable.DslTabLayout_Layout_layout_tab_height) + layoutConvexHeight = + a.getDimensionPixelOffset( + R.styleable.DslTabLayout_Layout_layout_tab_convex_height, + layoutConvexHeight + ) + indicatorContentIndex = a.getInt( + R.styleable.DslTabLayout_Layout_layout_tab_indicator_content_index, + indicatorContentIndex + ) + indicatorContentId = a.getResourceId( + R.styleable.DslTabLayout_Layout_layout_tab_indicator_content_id, + indicatorContentId + ) + weight = a.getFloat(R.styleable.DslTabLayout_Layout_layout_tab_weight, weight) + highlightDrawable = + a.getDrawable(R.styleable.DslTabLayout_Layout_layout_highlight_drawable) + + contentTextViewIndex = a.getInt( + R.styleable.DslTabLayout_Layout_layout_tab_text_view_index, + contentTextViewIndex + ) + contentIconViewIndex = a.getInt( + R.styleable.DslTabLayout_Layout_layout_tab_text_view_index, + contentIconViewIndex + ) + contentTextViewId = a.getResourceId( + R.styleable.DslTabLayout_Layout_layout_tab_text_view_id, + contentTextViewId + ) + contentIconViewId = a.getResourceId( + R.styleable.DslTabLayout_Layout_layout_tab_icon_view_id, + contentIconViewIndex + ) + + a.recycle() + + if (gravity == UNSPECIFIED_GRAVITY) { + gravity = if (layoutConvexHeight > 0) { + Gravity.BOTTOM + } else { + Gravity.CENTER + } + } + } + + constructor(source: ViewGroup.LayoutParams) : super(source) { + if (source is LayoutParams) { + this.layoutWidth = source.layoutWidth + this.layoutHeight = source.layoutHeight + this.layoutConvexHeight = source.layoutConvexHeight + this.weight = source.weight + this.highlightDrawable = source.highlightDrawable + } + } + + constructor(width: Int, height: Int) : super(width, height) + + constructor(width: Int, height: Int, gravity: Int) : super(width, height, gravity) + } + + // + + // + + //滚动支持 + val _overScroller: OverScroller by lazy { + OverScroller(context) + } + + //手势检测 + val _gestureDetector: GestureDetectorCompat by lazy { + GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (isHorizontal()) { + val absX = abs(velocityX) + if (absX > _minFlingVelocity) { + onFlingChange(velocityX) + } + } else { + val absY = abs(velocityY) + if (absY > _minFlingVelocity) { + onFlingChange(velocityY) + } + } + + return true + } + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + var handle = false + if (isHorizontal()) { + val absX = abs(distanceX) + if (absX > _touchSlop) { + handle = onScrollChange(distanceX) + } + } else { + val absY = abs(distanceY) + if (absY > _touchSlop) { + handle = onScrollChange(distanceY) + } + } + return handle + } + }) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + var intercept = false + if (needScroll) { + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + _overScroller.abortAnimation() + _scrollAnimator.cancel() + } + if (isEnabled) { + intercept = super.onInterceptTouchEvent(ev) || _gestureDetector.onTouchEvent(ev) + } + } else { + if (isEnabled) { + intercept = super.onInterceptTouchEvent(ev) + } + } + return if (isEnabled) { + if (itemEnableSelector) { + intercept + } else { + true + } + } else { + false + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (isEnabled) { + if (needScroll) { + _gestureDetector.onTouchEvent(event) + if (event.actionMasked == MotionEvent.ACTION_CANCEL || + event.actionMasked == MotionEvent.ACTION_UP + ) { + parent.requestDisallowInterceptTouchEvent(false) + } else if (event.actionMasked == MotionEvent.ACTION_DOWN) { + _overScroller.abortAnimation() + } + return true + } else { + return (isEnabled && super.onTouchEvent(event)) + } + } else { + return false + } + } + + /**是否需要滚动*/ + val needScroll: Boolean + get() = if (tabEnableSelectorMode) true else { + if (isHorizontal()) { + if (isLayoutRtl) { + minScrollX < 0 + } else { + maxScrollX > 0 + } + } else { + maxScrollY > 0 + } + } + + /**[parent]宽度外的滚动距离*/ + val maxScrollX: Int + get() = if (isLayoutRtl && isHorizontal()) { + if (tabEnableSelectorMode) viewDrawWidth / 2 else 0 + } else { + max( + maxWidth - measuredWidth + if (tabEnableSelectorMode) viewDrawWidth / 2 else 0, + 0 + ) + } + + val maxScrollY: Int + get() = max( + maxHeight - measuredHeight + if (tabEnableSelectorMode) viewDrawHeight / 2 else 0, + 0 + ) + + /**最小滚动的值*/ + val minScrollX: Int + get() = if (isLayoutRtl && isHorizontal()) { + min( + -(maxWidth - measuredWidth + if (tabEnableSelectorMode) viewDrawWidth / 2 else 0), + 0 + ) + } else { + if (tabEnableSelectorMode) -viewDrawWidth / 2 else 0 + } + + val minScrollY: Int + get() = if (tabEnableSelectorMode) -viewDrawHeight / 2 else 0 + + /**view最大的宽度*/ + val maxWidth: Int + get() = _childAllWidthSum + paddingStart + paddingEnd + + val maxHeight: Int + get() = _childAllWidthSum + paddingTop + paddingBottom + + open fun onFlingChange(velocity: Float /*瞬时值*/) { + if (needScroll) { + + //速率小于0 , 手指向左滑动 + //速率大于0 , 手指向右滑动 + + if (tabEnableSelectorMode) { + if (isHorizontal() && isLayoutRtl) { + if (velocity < 0) { + setCurrentItem(dslSelector.dslSelectIndex - 1) + } else if (velocity > 0) { + setCurrentItem(dslSelector.dslSelectIndex + 1) + } + } else { + if (velocity < 0) { + setCurrentItem(dslSelector.dslSelectIndex + 1) + } else if (velocity > 0) { + setCurrentItem(dslSelector.dslSelectIndex - 1) + } + } + } else { + if (isHorizontal()) { + if (isLayoutRtl) { + startFling(-velocity.toInt(), minScrollX, 0) + } else { + startFling(-velocity.toInt(), 0, maxScrollX) + } + } else { + startFling(-velocity.toInt(), 0, maxHeight) + } + } + } + } + + fun startFling(velocity: Int, min: Int, max: Int) { + + fun velocity(velocity: Int): Int { + return if (velocity > 0) { + clamp(velocity, _minFlingVelocity, _maxFlingVelocity) + } else { + clamp(velocity, -_maxFlingVelocity, -_minFlingVelocity) + } + } + + val v = velocity(velocity) + + _overScroller.abortAnimation() + + if (isHorizontal()) { + _overScroller.fling( + scrollX, + scrollY, + v, + 0, + min, + max, + 0, + 0, + measuredWidth, + 0 + ) + } else { + _overScroller.fling( + scrollX, + scrollY, + 0, + v, + 0, + 0, + min, + max, + 0, + measuredHeight + ) + } + postInvalidate() + } + + fun startScroll(dv: Int) { + _overScroller.abortAnimation() + if (isHorizontal()) { + _overScroller.startScroll(scrollX, scrollY, dv, 0, scrollAnimDuration) + } else { + _overScroller.startScroll(scrollX, scrollY, 0, dv, scrollAnimDuration) + } + ViewCompat.postInvalidateOnAnimation(this) + } + + /**检查是否需要重置滚动的位置*/ + fun restoreScroll() { + if (itemIsEquWidth || !needScroll) { + if (scrollX != 0 || scrollY != 0) { + scrollTo(0, 0) + } + } + } + + open fun onScrollChange(distance: Float): Boolean { + if (needScroll) { + + //distance小于0 , 手指向右滑动 + //distance大于0 , 手指向左滑动 + + parent.requestDisallowInterceptTouchEvent(true) + + if (tabEnableSelectorMode) { + //滑动选择模式下, 不响应scroll事件 + } else { + if (isHorizontal()) { + scrollBy(distance.toInt(), 0) + } else { + scrollBy(0, distance.toInt()) + } + } + return true + } + return false + } + + override fun scrollTo(x: Int, y: Int) { + if (isHorizontal()) { + when { + x > maxScrollX -> super.scrollTo(maxScrollX, 0) + x < minScrollX -> super.scrollTo(minScrollX, 0) + else -> super.scrollTo(x, 0) + } + } else { + when { + y > maxScrollY -> super.scrollTo(0, maxScrollY) + y < minScrollY -> super.scrollTo(0, minScrollY) + else -> super.scrollTo(0, y) + } + } + } + + override fun computeScroll() { + if (_overScroller.computeScrollOffset()) { + scrollTo(_overScroller.currX, _overScroller.currY) + invalidate() + if (_overScroller.currX < minScrollX || _overScroller.currX > maxScrollX) { + _overScroller.abortAnimation() + } + } + } + + fun _getViewTargetX(): Int { + return when (tabIndicator.indicatorGravity) { + DslTabIndicator.INDICATOR_GRAVITY_START -> paddingStart + DslTabIndicator.INDICATOR_GRAVITY_END -> measuredWidth - paddingEnd + else -> paddingStart + viewDrawWidth / 2 + } + } + + fun _getViewTargetY(): Int { + return when (tabIndicator.indicatorGravity) { + DslTabIndicator.INDICATOR_GRAVITY_START -> paddingTop + DslTabIndicator.INDICATOR_GRAVITY_END -> measuredHeight - paddingBottom + else -> paddingTop + viewDrawHeight / 2 + } + } + + /**将[index]位置显示在TabLayout的中心*/ + fun _scrollToTarget(index: Int, scrollAnim: Boolean) { + if (!needScroll) { + return + } + + dslSelector.visibleViewList.getOrNull(index)?.let { + if (!ViewCompat.isLaidOut(it)) { + //没有布局 + return + } + } + + val dv = if (isHorizontal()) { + val childTargetX = tabIndicator.getChildTargetX(index) + val viewDrawTargetX = _getViewTargetX() + when { + tabEnableSelectorMode -> { + val viewCenterX = measuredWidth / 2 + childTargetX - viewCenterX - scrollX + } + + isLayoutRtl -> { + if (childTargetX < viewDrawTargetX) { + childTargetX - viewDrawTargetX - scrollX + } else { + -scrollX + } + } + + else -> { + if (childTargetX > viewDrawTargetX) { + childTargetX - viewDrawTargetX - scrollX + } else { + -scrollX + } + } + } + } else { + //竖向 + val childTargetY = tabIndicator.getChildTargetY(index) + val viewDrawTargetY = _getViewTargetY() + when { + tabEnableSelectorMode -> { + val viewCenterY = measuredHeight / 2 + childTargetY - viewCenterY - scrollY + } + + childTargetY > viewDrawTargetY -> { + childTargetY - viewDrawTargetY - scrollY + } + + else -> { + if (tabIndicator.indicatorGravity == DslTabIndicator.INDICATOR_GRAVITY_END && + childTargetY < viewDrawTargetY + ) { + childTargetY - viewDrawTargetY - scrollY + } else { + -scrollY + } + } + } + } + + if (isHorizontal()) { + if (isInEditMode || !scrollAnim) { + _overScroller.abortAnimation() + scrollBy(dv, 0) + } else { + startScroll(dv) + } + } else { + if (isInEditMode || !scrollAnim) { + _overScroller.abortAnimation() + scrollBy(0, dv) + } else { + startScroll(dv) + } + } + } + + // + + // + + val _scrollAnimator: ValueAnimator by lazy { + ValueAnimator().apply { + interpolator = LinearInterpolator() + duration = tabIndicatorAnimationDuration + addUpdateListener { + _onAnimateValue(it.animatedValue as Float) + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator) { + _onAnimateValue(1f) + onAnimationEnd(animation) + } + + override fun onAnimationEnd(animation: Animator) { + _onAnimateEnd() + } + }) + } + } + + val isAnimatorStart: Boolean + get() = _scrollAnimator.isStarted + + fun _animateToItem(fromIndex: Int, toIndex: Int) { + if (toIndex == fromIndex) { + return + } + + //取消之前的动画 + _scrollAnimator.cancel() + + if (!tabIndicator.indicatorAnim) { + //不需要动画 + _onAnimateEnd() + return + } + + if (fromIndex < 0) { + //从一个不存在的位置, 到目标位置 + tabIndicator.currentIndex = toIndex + } else { + tabIndicator.currentIndex = fromIndex + } + tabIndicator._targetIndex = toIndex + + if (isInEditMode) { + tabIndicator.currentIndex = toIndex + return + } + + if (tabIndicator.currentIndex == tabIndicator._targetIndex) { + return + } + //"_animateToItem ${tabIndicator.currentIndex} ${tabIndicator._targetIndex}".loge() + _scrollAnimator.setFloatValues(tabIndicator.positionOffset, 1f) + _scrollAnimator.start() + } + + fun _onAnimateValue(value: Float) { + tabIndicator.positionOffset = value + tabLayoutConfig?.onPageIndexScrolled( + tabIndicator.currentIndex, + tabIndicator._targetIndex, + value + ) + tabLayoutConfig?.let { + dslSelector.visibleViewList.apply { + val targetView = getOrNull(tabIndicator._targetIndex) + if (targetView != null) { + it.onPageViewScrolled( + getOrNull(tabIndicator.currentIndex), + targetView, + value + ) + } + } + } + } + + fun _onAnimateEnd() { + tabIndicator.currentIndex = dslSelector.dslSelectIndex + tabIndicator._targetIndex = tabIndicator.currentIndex + tabIndicator.positionOffset = 0f + //结束_viewPager的滚动动画, 系统没有直接结束的api, 固用此方法代替. + //_viewPager?.setCurrentItem(tabIndicator.currentIndex, false) + } + + // + + // + + var _viewPagerDelegate: ViewPagerDelegate? = null + var _viewPagerScrollState = 0 + + fun onPageScrollStateChanged(state: Int) { + //"$state".logi() + _viewPagerScrollState = state + if (state == ViewPagerDelegate.SCROLL_STATE_IDLE) { + _onAnimateEnd() + dslSelector.updateStyle() + } + } + + fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + if (isAnimatorStart) { + //动画已经开始了 + return + } + + val currentItem = _viewPagerDelegate?.onGetCurrentItem() ?: 0 + //"$currentItem:$position $positionOffset $positionOffsetPixels state:$_viewPagerScrollState".logw() + + if (position < currentItem) { + //Page 目标在左 + if (_viewPagerScrollState == ViewPagerDelegate.SCROLL_STATE_DRAGGING) { + tabIndicator.currentIndex = position + 1 + tabIndicator._targetIndex = position + } + _onAnimateValue(1 - positionOffset) + } else { + //Page 目标在右 + if (_viewPagerScrollState == ViewPagerDelegate.SCROLL_STATE_DRAGGING) { + tabIndicator.currentIndex = position + tabIndicator._targetIndex = position + 1 + } + _onAnimateValue(positionOffset) + } + } + + fun onPageSelected(position: Int) { + setCurrentItem(position, true, false) + } + + // + + // + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is Bundle) { + val oldState: Parcelable? = state.getParcelable("old") + super.onRestoreInstanceState(oldState) + + tabDefaultIndex = state.getInt("defaultIndex", tabDefaultIndex) + val currentItemIndex = state.getInt("currentIndex", -1) + dslSelector.dslSelectIndex = -1 + if (currentItemIndex > 0) { + setCurrentItem(currentItemIndex, true, false) + } + } else { + super.onRestoreInstanceState(state) + } + } + + override fun onSaveInstanceState(): Parcelable? { + val state = super.onSaveInstanceState() + val bundle = Bundle() + bundle.putParcelable("old", state) + bundle.putInt("defaultIndex", tabDefaultIndex) + bundle.putInt("currentIndex", currentItemIndex) + return bundle + } + + // +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayoutConfig.kt b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayoutConfig.kt new file mode 100644 index 000000000..3d4a48dec --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayoutConfig.kt @@ -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(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(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(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(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(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(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) + ) + } + } +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/ITabIndicatorDraw.kt b/TabLayout/src/main/java/com/angcyo/tablayout/ITabIndicatorDraw.kt new file mode 100644 index 000000000..1b7f74926 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/ITabIndicatorDraw.kt @@ -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 + ) + +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/LibEx.kt b/TabLayout/src/main/java/com/angcyo/tablayout/LibEx.kt new file mode 100644 index 000000000..2d040a90e --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/LibEx.kt @@ -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(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(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 List?.isChange(other: List?): Boolean { + if (this.size() != other.size()) { + return true + } + this?.forEachIndexed { index, t -> + if (t != other?.getOrNull(index)) { + return true + } + } + return false +} + +fun 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 +} \ No newline at end of file diff --git a/TabLayout/src/main/java/com/angcyo/tablayout/ViewPagerDelegate.kt b/TabLayout/src/main/java/com/angcyo/tablayout/ViewPagerDelegate.kt new file mode 100644 index 000000000..a40df8500 --- /dev/null +++ b/TabLayout/src/main/java/com/angcyo/tablayout/ViewPagerDelegate.kt @@ -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) +} \ No newline at end of file diff --git a/TabLayout/src/main/res/values/attr_dsl_tab_layout.xml b/TabLayout/src/main/res/values/attr_dsl_tab_layout.xml new file mode 100644 index 000000000..423d215c8 --- /dev/null +++ b/TabLayout/src/main/res/values/attr_dsl_tab_layout.xml @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle index 39c0f0dd1..664aa81d8 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -107,6 +107,7 @@ dependencies { //svga播放器 //api rootProject.ext.dependencies["SVGAPlayer"] + // api project(path:':SVGAlibrary')//svga implementation 'com.squareup.wire:wire-runtime:4.4.1' //七牛云存储 @@ -237,4 +238,7 @@ dependencies { api 'com.google.android.exoplayer:exoplayer-core:2.18.2@aar' api rootProject.ext.dependencies["blank-utilcode"] + + //下标切换器 https://github.com/angcyo/DslTabLayout + api project(':TabLayout') } diff --git a/common/src/main/assets/gift_wall_gift_info_light.svga b/common/src/main/assets/gift_wall_gift_info_light.svga new file mode 100644 index 000000000..6965e8c72 Binary files /dev/null and b/common/src/main/assets/gift_wall_gift_info_light.svga differ diff --git a/common/src/main/assets/gift_wall_light_up.svga b/common/src/main/assets/gift_wall_light_up.svga new file mode 100644 index 000000000..d62328461 Binary files /dev/null and b/common/src/main/assets/gift_wall_light_up.svga differ diff --git a/common/src/main/java/com/yunbao/common/Constants.java b/common/src/main/java/com/yunbao/common/Constants.java index 58bbd92e6..a2bab93b2 100644 --- a/common/src/main/java/com/yunbao/common/Constants.java +++ b/common/src/main/java/com/yunbao/common/Constants.java @@ -20,6 +20,7 @@ public class Constants { public static final String AVATAR = "avatar"; public static final String SIGN = "sign"; public static final String TO_UID = "toUid"; + public static final String TO_UNAME = "toUserName"; public static final String INTOINDEX = "intoIndex"; public static final String FROM_LIVE_ROOM = "fromLiveRoom"; public static final String TO_NAME = "toName"; diff --git a/common/src/main/java/com/yunbao/common/adapter/GiftWallGiftInfoListItemAdapter.java b/common/src/main/java/com/yunbao/common/adapter/GiftWallGiftInfoListItemAdapter.java new file mode 100644 index 000000000..1720818ca --- /dev/null +++ b/common/src/main/java/com/yunbao/common/adapter/GiftWallGiftInfoListItemAdapter.java @@ -0,0 +1,77 @@ +package com.yunbao.common.adapter; + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.makeramen.roundedimageview.RoundedImageView; +import com.yunbao.common.R; +import com.yunbao.common.bean.GiftWallInfoBean; +import com.yunbao.common.glide.ImgLoader; +import com.yunbao.common.utils.WordUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class GiftWallGiftInfoListItemAdapter extends RecyclerView.Adapter { + List data = new ArrayList<>(); + + public void setData(List data) { + if (data == null) { + data = new ArrayList<>(); + } + this.data = data; + notifyDataSetChanged(); + } + + @NonNull + @Override + public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new VH(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_dialog_gift_wall_gift_info, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull VH holder, int position) { + holder.setData(data.get(position), position); + } + + @Override + public int getItemCount() { + return data.size(); + } + + public class VH extends RecyclerView.ViewHolder { + TextView tv_rank, user_name, tv_rename; + RoundedImageView avatar; + + public VH(@NonNull View itemView) { + super(itemView); + tv_rank = itemView.findViewById(R.id.tv_rank); + user_name = itemView.findViewById(R.id.user_name); + tv_rename = itemView.findViewById(R.id.tv_rename); + avatar = itemView.findViewById(R.id.avatar); + } + + public void setData(GiftWallInfoBean.Data data, int position) { + tv_rank.setText(String.format(Locale.getDefault(), "%d", (position + 1))); + tv_rank.setTextColor(Color.parseColor("#FCC755")); + tv_rank.setTextSize(20); + tv_rename.setText(String.format(Locale.getDefault(), "%d", (data.getGift_hall_send_num()))); + avatar.setVisibility(View.VISIBLE); + if (data.getActive_rank_hide() == 1) { + user_name.setText(WordUtil.getNewString(R.string.mystery_man)); + avatar.setImageResource(R.mipmap.hide); + } else { + user_name.setText(data.getUser_name()); + ImgLoader.display(itemView.getContext(), data.getAvatar(), avatar); + } + } + + } +} diff --git a/common/src/main/java/com/yunbao/common/adapter/GiftWallMainTab1List2Adapter.java b/common/src/main/java/com/yunbao/common/adapter/GiftWallMainTab1List2Adapter.java new file mode 100644 index 000000000..3d8f9c07c --- /dev/null +++ b/common/src/main/java/com/yunbao/common/adapter/GiftWallMainTab1List2Adapter.java @@ -0,0 +1,180 @@ +package com.yunbao.common.adapter; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.opensource.svgaplayer.SVGACallback; +import com.opensource.svgaplayer.SVGADrawable; +import com.opensource.svgaplayer.SVGAImageView; +import com.opensource.svgaplayer.SVGAParser; +import com.opensource.svgaplayer.SVGAVideoEntity; +import com.yunbao.common.R; +import com.yunbao.common.bean.GiftWallBean; +import com.yunbao.common.dialog.GiftWallGiftInfoDialog; +import com.yunbao.common.glide.ImgLoader; +import com.yunbao.common.manager.base.ACache; +import com.yunbao.common.utils.DpUtil; +import com.yunbao.common.utils.SVGAViewUtils; +import com.yunbao.common.utils.WordUtil; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +import java.util.ArrayList; +import java.util.List; + +public class GiftWallMainTab1List2Adapter extends RecyclerView.Adapter { + Context mContext; + List list; + boolean isStar; + SVGAVideoEntity drawable; + String toUid; + boolean isAnchor; + private boolean isLiveRoom; + + public GiftWallMainTab1List2Adapter(Context mContext) { + this.mContext = mContext; + list = new ArrayList<>(); + } + + public void setToUid(String toUid) { + this.toUid = toUid; + } + + public void setAnchor(boolean anchor) { + isAnchor = anchor; + } + + public void setList(List list) { + if (list == null) { + list = new ArrayList<>(); + } + this.list = list; + notifyDataSetChanged(); + } + + public void setDrawable(SVGAVideoEntity drawable) { + this.drawable = drawable; + } + + public void setStar(boolean star) { + isStar = star; + } + + @NonNull + @Override + public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + return new VH(LayoutInflater.from(mContext).inflate(R.layout.item_gift_wall_man_tab1_list_2, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull VH holder, int position) { + holder.setData(list.get(position), position); + } + + @Override + public int getItemCount() { + return list.size(); + } + + @Override + public void onViewAttachedToWindow(@NonNull VH holder) { + super.onViewAttachedToWindow(holder); + holder.giftBg.startAnimation(); + } + + + @Override + public void onViewDetachedFromWindow(@NonNull VH holder) { + super.onViewDetachedFromWindow(holder); + holder.giftBg.stopAnimation(); + + } + + public void setLiveRoom(boolean isLiveRoom) { + this.isLiveRoom = isLiveRoom; + } + + public class VH extends RecyclerView.ViewHolder { + SVGAImageView giftBg; + ImageView gift_soles; + ImageView giftImage; + TextView gift_name; + TextView gift_status; + ProgressBar progressBar; + + public VH(View itemView) { + super(itemView); + giftBg = itemView.findViewById(R.id.gift_bg); + gift_soles = itemView.findViewById(R.id.gift_soles); + giftImage = itemView.findViewById(R.id.gift); + gift_name = itemView.findViewById(R.id.gift_name); + gift_status = itemView.findViewById(R.id.gift_status); + progressBar = itemView.findViewById(R.id.progressBar); + } + + @SuppressLint("DefaultLocale") + public void setData(GiftWallBean.Gift gift, int position) { + gift_name.setText(WordUtil.isNewZh() ? gift.getGift_name() : gift.getGift_name_en()); + ImgLoader.display(itemView.getContext(), gift.getGift_icon(), giftImage, 40, 40); + giftBg.setClearsAfterDetached(false); + giftBg.setClearsAfterStop(false); + if (gift.getIlluminate_status() == 1) { + gift_status.setText(String.format("%s%d", WordUtil.getNewString(R.string.dialog_gift_wall_list_spinner_up), gift.getGift_hall_send_num())); + gift_status.setTextColor(Color.parseColor("#FFFFFF")); + gift_soles.setImageResource(getSolesrRes()); + if (drawable != null) { + giftBg.post(() -> { + giftBg.setImageDrawable(new SVGADrawable(drawable)); + giftBg.setLoops(0); + giftBg.startAnimation(); + }); + } + progressBar.setMax(gift.getIlluminate_num()); + progressBar.setProgress(gift.getGift_hall_send_num()); + } else { + gift_status.setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_spinner_down)); + gift_status.setTextColor(Color.parseColor("#01071A")); + gift_soles.setImageResource(getUnSolesrRes()); + giftBg.setImageResource(R.mipmap.gift_wall_main_item_bg1); + } + ViewClicksAntiShake.clicksAntiShake(itemView, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + new GiftWallGiftInfoDialog(mContext, gift.getGift_id() + "", toUid, isAnchor) + .setFullWindows(!isLiveRoom) + .setLiveRoom(isLiveRoom) + .setStar(isStar) + .showDialog(); + } + }); + } + + private int getSolesrRes() { + return isStar ? R.mipmap.gift_wall_main_item_select : R.mipmap.gift_wall_main_item_select1; + } + + private int getUnSolesrRes() { + return isStar ? R.mipmap.gift_wall_main_item_unselect : R.mipmap.gift_wall_main_item_unselect1; + } + } +} diff --git a/common/src/main/java/com/yunbao/common/adapter/GiftWallMainTab2ListAdapter.java b/common/src/main/java/com/yunbao/common/adapter/GiftWallMainTab2ListAdapter.java new file mode 100644 index 000000000..1c11ab4dd --- /dev/null +++ b/common/src/main/java/com/yunbao/common/adapter/GiftWallMainTab2ListAdapter.java @@ -0,0 +1,169 @@ +package com.yunbao.common.adapter; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.makeramen.roundedimageview.RoundedImageView; +import com.opensource.svgaplayer.SVGADrawable; +import com.opensource.svgaplayer.SVGAImageView; +import com.opensource.svgaplayer.SVGAVideoEntity; +import com.yunbao.common.R; +import com.yunbao.common.bean.GiftWallBean; +import com.yunbao.common.bean.GiftWallTab2Bean; +import com.yunbao.common.dialog.GiftWallGiftInfoDialog; +import com.yunbao.common.dialog.GiftWallMainTab2ClassicInfoDialog; +import com.yunbao.common.glide.ImgLoader; +import com.yunbao.common.utils.StringUtil; +import com.yunbao.common.utils.WordUtil; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class GiftWallMainTab2ListAdapter extends RecyclerView.Adapter { + Context mContext; + List list; + boolean isStar; + String toUid; + boolean isAnchor; + private boolean isLiveRoom; + + public GiftWallMainTab2ListAdapter(Context mContext) { + this.mContext = mContext; + list = new ArrayList<>(); + } + + public void setToUid(String toUid) { + this.toUid = toUid; + } + + public void setAnchor(boolean anchor) { + isAnchor = anchor; + } + + public void setList(List list) { + if (list == null) { + list = new ArrayList<>(); + } + this.list = list; + notifyDataSetChanged(); + } + + + public void setStar(boolean star) { + isStar = star; + } + + @NonNull + @Override + public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (isStar) { + return new VH(LayoutInflater.from(mContext).inflate(R.layout.item_gift_wall_man_tab2_list_1, parent, false)); + } + return new VH(LayoutInflater.from(mContext).inflate(R.layout.item_gift_wall_man_tab2_list_2, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull VH holder, int position) { + holder.setData(list.get(position), position); + } + + @Override + public int getItemCount() { + return list.size(); + } + + @Override + public void onViewAttachedToWindow(@NonNull VH holder) { + super.onViewAttachedToWindow(holder); + } + + + @Override + public void onViewDetachedFromWindow(@NonNull VH holder) { + super.onViewDetachedFromWindow(holder); + + } + + public void setLiveRoom(boolean isLiveRoom) { + this.isLiveRoom = isLiveRoom; + } + + public class VH extends RecyclerView.ViewHolder { + ImageView gift; + TextView gift_name; + TextView gift_number; + RoundedImageView user1Avatar, user2Avatar; + TextView anchor_nickname; + TextView user_nickname; + TextView tv_wait; + View imageView8; + + public VH(View itemView) { + super(itemView); + + gift = itemView.findViewById(R.id.gift); + gift_name = itemView.findViewById(R.id.gift_name); + gift_number = itemView.findViewById(R.id.gift_number); + user1Avatar = itemView.findViewById(R.id.user1_avatar); + user2Avatar = itemView.findViewById(R.id.user2_avatar); + anchor_nickname = itemView.findViewById(R.id.anchor_nickname); + user_nickname = itemView.findViewById(R.id.user_nickname); + tv_wait = itemView.findViewById(R.id.tv_wait); + imageView8 = itemView.findViewById(R.id.imageView8); + } + + @SuppressLint("DefaultLocale") + public void setData(GiftWallTab2Bean.Gift giftData, int position) { + gift_name.setText(WordUtil.isNewZh() ? giftData.getGiftName() : giftData.getGiftNameEn()); + ImgLoader.display(itemView.getContext(), giftData.getGiftIcon(), gift, 60, 60); + if (!StringUtil.isEmpty(giftData.getNamingLiveNicename(), giftData.getNamingUserNicename())) { + anchor_nickname.setVisibility(View.VISIBLE); + user_nickname.setVisibility(View.VISIBLE); + gift_number.setVisibility(View.VISIBLE); + user1Avatar.setVisibility(View.VISIBLE); + user2Avatar.setVisibility(View.VISIBLE); + imageView8.setVisibility(View.VISIBLE); + tv_wait.setVisibility(View.GONE); + gift_number.setText(String.format(Locale.getDefault(), "%d", giftData.getNeedCoinTotal())); + ImgLoader.display(mContext, giftData.getNamingLiveAvatar(), user1Avatar, 35, 35); + ImgLoader.display(mContext, giftData.getNamingUserAvatar(), user2Avatar, 35, 35); + anchor_nickname.setText(giftData.getNamingLiveNicename()); + user_nickname.setText(giftData.getNamingUserNicename()); + } else { + user1Avatar.setVisibility(View.GONE); + user2Avatar.setVisibility(View.GONE); + anchor_nickname.setVisibility(View.GONE); + user_nickname.setVisibility(View.GONE); + gift_number.setVisibility(View.GONE); + imageView8.setVisibility(View.GONE); + tv_wait.setVisibility(View.VISIBLE); + } + ViewClicksAntiShake.clicksAntiShake(itemView, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + new GiftWallMainTab2ClassicInfoDialog(mContext, giftData, isAnchor).setFullWindows(!isLiveRoom).show(); + } + }); + } + + private int getSolesrRes() { + return isStar ? R.mipmap.gift_wall_main_item_select : R.mipmap.gift_wall_main_item_select1; + } + + private int getUnSolesrRes() { + return isStar ? R.mipmap.gift_wall_main_item_unselect : R.mipmap.gift_wall_main_item_unselect1; + } + } +} diff --git a/common/src/main/java/com/yunbao/common/bean/GiftWallBean.java b/common/src/main/java/com/yunbao/common/bean/GiftWallBean.java new file mode 100644 index 000000000..32d35b354 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/bean/GiftWallBean.java @@ -0,0 +1,253 @@ +package com.yunbao.common.bean; + +import com.google.gson.annotations.SerializedName; +import com.yunbao.common.utils.StringUtil; + +import java.util.List; + +public class GiftWallBean extends BaseModel { + private IlluminateData illuminate_data; + private int active_rank_hide; + + + public GiftWallBean() { + } + + public IlluminateData getIlluminate_data() { + return illuminate_data; + } + + public void setIlluminate_data(IlluminateData illuminate_data) { + this.illuminate_data = illuminate_data; + } + + public int getActive_rank_hide() { + return active_rank_hide; + } + + public void setActive_rank_hide(int active_rank_hide) { + this.active_rank_hide = active_rank_hide; + } + + public static class IlluminateData { + private List week_star_data; + private List gift_data; + private int week_gift_illuminate_num; + private int week_star_gift_num; + private int gift_illuminate_num; + private int gift_num; + private String gift_hall_start_date; + private String gift_hall_end_date; + + + public IlluminateData() { + } + + public String getGift_hall_start_date() { + if(!StringUtil.isEmpty("gift_hall_start_date")){ + gift_hall_start_date=gift_hall_start_date.replace("-","/"); + } + return gift_hall_start_date; + } + + public void setGift_hall_start_date(String gift_hall_start_date) { + this.gift_hall_start_date = gift_hall_start_date; + } + + public String getGift_hall_end_date() { + if(!StringUtil.isEmpty("gift_hall_end_date")){ + gift_hall_end_date=gift_hall_end_date.replace("-","/"); + } + return gift_hall_end_date; + } + + public void setGift_hall_end_date(String gift_hall_end_date) { + this.gift_hall_end_date = gift_hall_end_date; + } + + public List getWeek_star_data() { + return week_star_data; + } + + public void setWeek_star_data(List week_star_data) { + this.week_star_data = week_star_data; + } + + public List getGift_data() { + return gift_data; + } + + public void setGift_data(List gift_data) { + this.gift_data = gift_data; + } + + public int getWeek_gift_illuminate_num() { + return week_gift_illuminate_num; + } + + public void setWeek_gift_illuminate_num(int week_gift_illuminate_num) { + this.week_gift_illuminate_num = week_gift_illuminate_num; + } + + public int getWeek_star_gift_num() { + return week_star_gift_num; + } + + public void setWeek_star_gift_num(int week_star_gift_num) { + this.week_star_gift_num = week_star_gift_num; + } + + public int getGift_illuminate_num() { + return gift_illuminate_num; + } + + public void setGift_illuminate_num(int gift_illuminate_num) { + this.gift_illuminate_num = gift_illuminate_num; + } + + public int getGift_num() { + return gift_num; + } + + public void setGift_num(int gift_num) { + this.gift_num = gift_num; + } + } + + public static class Gift { + private int gift_id; + private int sendtype; + private String gift_name; + private long need_coin; // 注意:这里可能需要更改为long类型,因为666666超过了int的最大值 + private String gift_icon; + private int week_start_level; + private int illuminate_num; + private int gift_hall_type; + private String gift_name_en; + private String gift_hall_start; // 使用Java 8的LocalDate来处理日期 + private String gift_hall_end; + private int gift_hall_send_num; + private int illuminate_status; + private long needcoin_total; + + public Gift() { + } + + public String getGift_hall_start() { + if(!StringUtil.isEmpty(gift_hall_start)){ + gift_hall_start=gift_hall_start.replace("-","/"); + } + return gift_hall_start; + } + public String getGift_hall_end() { + if(!StringUtil.isEmpty(gift_hall_end)){ + gift_hall_end=gift_hall_end.replace("-","/"); + } + return gift_hall_end; + } + public int getGift_id() { + return gift_id; + } + + public void setGift_id(int gift_id) { + this.gift_id = gift_id; + } + + public int getSendtype() { + return sendtype; + } + + public void setSendtype(int sendtype) { + this.sendtype = sendtype; + } + + public String getGift_name() { + return gift_name; + } + + public void setGift_name(String gift_name) { + this.gift_name = gift_name; + } + + public long getNeed_coin() { + return need_coin; + } + + public void setNeed_coin(long need_coin) { + this.need_coin = need_coin; + } + + public String getGift_icon() { + return gift_icon; + } + + public void setGift_icon(String gift_icon) { + this.gift_icon = gift_icon; + } + + public int getWeek_start_level() { + return week_start_level; + } + + public void setWeek_start_level(int week_start_level) { + this.week_start_level = week_start_level; + } + + public int getIlluminate_num() { + return illuminate_num; + } + + public void setIlluminate_num(int illuminate_num) { + this.illuminate_num = illuminate_num; + } + + public int getGift_hall_type() { + return gift_hall_type; + } + + public void setGift_hall_type(int gift_hall_type) { + this.gift_hall_type = gift_hall_type; + } + + public String getGift_name_en() { + return gift_name_en; + } + + public void setGift_name_en(String gift_name_en) { + this.gift_name_en = gift_name_en; + } + + public void setGift_hall_start(String gift_hall_start) { + this.gift_hall_start = gift_hall_start; + } + + + public void setGift_hall_end(String gift_hall_end) { + this.gift_hall_end = gift_hall_end; + } + + public int getGift_hall_send_num() { + return gift_hall_send_num; + } + + public void setGift_hall_send_num(int gift_hall_send_num) { + this.gift_hall_send_num = gift_hall_send_num; + } + + public int getIlluminate_status() { + return illuminate_status; + } + + public void setIlluminate_status(int illuminate_status) { + this.illuminate_status = illuminate_status; + } + + public long getNeedcoin_total() { + return needcoin_total; + } + + public void setNeedcoin_total(long needcoin_total) { + this.needcoin_total = needcoin_total; + } + } +} diff --git a/common/src/main/java/com/yunbao/common/bean/GiftWallForUserBean.java b/common/src/main/java/com/yunbao/common/bean/GiftWallForUserBean.java new file mode 100644 index 000000000..efe70ed51 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/bean/GiftWallForUserBean.java @@ -0,0 +1,114 @@ +package com.yunbao.common.bean; + +import java.util.List; + +public class GiftWallForUserBean extends BaseModel { + private List illuminate_data; + private int active_rank_hide; + + public GiftWallForUserBean() { + } + + public List getIlluminate_data() { + return illuminate_data; + } + + public void setIlluminate_data(List illuminate_data) { + this.illuminate_data = illuminate_data; + } + + public int getActive_rank_hide() { + return active_rank_hide; + } + + public void setActive_rank_hide(int active_rank_hide) { + this.active_rank_hide = active_rank_hide; + } + + public static class Gift { + private String giftname; + private String giftname_en; + private String gifticon; + private int needcoin; + private int needcoin_total; + private int gift_hall_send_num; + private int illuminate_status; + private int gift_id; + private int id; + + public Gift() { + } + + public String getGiftname() { + return giftname; + } + + public void setGiftname(String giftname) { + this.giftname = giftname; + } + + public String getGiftname_en() { + return giftname_en; + } + + public void setGiftname_en(String giftname_en) { + this.giftname_en = giftname_en; + } + + public String getGifticon() { + return gifticon; + } + + public void setGifticon(String gifticon) { + this.gifticon = gifticon; + } + + public int getNeedcoin() { + return needcoin; + } + + public void setNeedcoin(int needcoin) { + this.needcoin = needcoin; + } + + public int getNeedcoin_total() { + return needcoin_total; + } + + public void setNeedcoin_total(int needcoin_total) { + this.needcoin_total = needcoin_total; + } + + public int getGift_hall_send_num() { + return gift_hall_send_num; + } + + public void setGift_hall_send_num(int gift_hall_send_num) { + this.gift_hall_send_num = gift_hall_send_num; + } + + public int getIlluminate_status() { + return illuminate_status; + } + + public void setIlluminate_status(int illuminate_status) { + this.illuminate_status = illuminate_status; + } + + public int getGift_id() { + return gift_id; + } + + public void setGift_id(int gift_id) { + this.gift_id = gift_id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + } +} diff --git a/common/src/main/java/com/yunbao/common/bean/GiftWallInfoBean.java b/common/src/main/java/com/yunbao/common/bean/GiftWallInfoBean.java new file mode 100644 index 000000000..686263037 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/bean/GiftWallInfoBean.java @@ -0,0 +1,258 @@ +package com.yunbao.common.bean; + +import com.alibaba.fastjson.annotation.JSONField; +import com.google.gson.annotations.SerializedName; +import com.yunbao.common.utils.RandomUtil; +import com.yunbao.common.utils.StringUtil; +import com.yunbao.common.utils.WordUtil; + +import java.util.ArrayList; +import java.util.List; + +public class GiftWallInfoBean extends BaseModel { + @SerializedName("gift_info") + private GiftInfo gift_info; + @SerializedName("data") + private List data; + @SerializedName("is_me") + private int is_me; + + // 一般情况下,我们会添加getter和setter方法,但根据你的要求,这里省略 + + + @JSONField(name = "gift_info") + public GiftInfo getGift_info() { + return gift_info; + } + + @JSONField(name = "gift_info") + public void setGift_info(GiftInfo gift_info) { + this.gift_info = gift_info; + } + + @JSONField(name = "data") + public List getData() { + /* if (data == null || data.isEmpty()) { + data = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Data item = new Data(); + item.setId(i); + item.setActive_rank_hide(i % 4 == 0 ? 0 : 1); + item.setUser_name("用户:" + i); + item.setGift_hall_send_num(RandomUtil.nextInt(100000)); + item.setAvatar("https://downs.yaoulive.com/manfive.png"); + data.add(item); + } + }*/ + return data; + } + + @JSONField(name = "data") + public void setData(List data) { + this.data = data; + } + + public int getIs_me() { + return is_me; + } + + public void setIs_me(int is_me) { + this.is_me = is_me; + } + + // 嵌套类:GiftInfo + public static class GiftInfo { + @SerializedName("gift_name") + private String giftname; + @SerializedName("gift_name_en") + private String giftname_en; + @SerializedName("gift_icon") + private String gifticon; + @SerializedName("need_coin") + private int needcoin; + @SerializedName("needcoin_total") + private int needcoin_total; + @SerializedName("gift_hall_send_num") + private String gift_hall_send_num; + @SerializedName("illuminate_num") + private int illuminate_num; + @SerializedName("illuminate_status") + private int illuminate_status; + @SerializedName("gift_id") + private int gift_id; + @SerializedName("id") + private int id; + @SerializedName("gift_hall_start_date") + private String gift_hall_start; // 使用Java 8的LocalDate来处理日期 + @SerializedName("gift_hall_end_date") + private String gift_hall_end; + + @JSONField(name = "gift_hall_start_date") + public void setGift_hall_start(String gift_hall_start) { + this.gift_hall_start = gift_hall_start; + } + @JSONField(name = "gift_hall_start_date") + public String getGift_hall_start() { + if(!StringUtil.isEmpty(gift_hall_start)){ + gift_hall_start=gift_hall_start.replace("-","/"); + } + return gift_hall_start; + } + @JSONField(name = "gift_hall_end_date") + public String getGift_hall_end() { + if(!StringUtil.isEmpty(gift_hall_end)){ + gift_hall_end=gift_hall_end.replace("-","/"); + } + return gift_hall_end; + } + + public void setGift_hall_end(String gift_hall_end) { + this.gift_hall_end = gift_hall_end; + } + + // 同样地,这里省略getter和setter方法 + + public String getGiftname() { + return giftname; + } + + public void setGiftname(String giftname) { + this.giftname = giftname; + } + + public String getGiftname_en() { + return giftname_en; + } + + public void setGiftname_en(String giftname_en) { + this.giftname_en = giftname_en; + } + + public String getGifticon() { + return gifticon; + } + + public void setGifticon(String gifticon) { + this.gifticon = gifticon; + } + + public int getNeedcoin() { + return needcoin; + } + + public void setNeedcoin(int needcoin) { + this.needcoin = needcoin; + } + + public int getNeedcoin_total() { + return needcoin_total; + } + + public void setNeedcoin_total(int needcoin_total) { + this.needcoin_total = needcoin_total; + } + + public int getIlluminate_num() { + return illuminate_num; + } + + public void setIlluminate_num(int illuminate_num) { + this.illuminate_num = illuminate_num; + } + + public String getGift_hall_send_num() { + if (StringUtil.isEmpty(gift_hall_send_num)) { + gift_hall_send_num = "0"; + } + return gift_hall_send_num; + } + + public void setGift_hall_send_num(String gift_hall_send_num) { + this.gift_hall_send_num = gift_hall_send_num; + } + + public int getIlluminate_status() { + return illuminate_status; + } + + public void setIlluminate_status(int illuminate_status) { + this.illuminate_status = illuminate_status; + } + + public int getGift_id() { + return gift_id; + } + + public void setGift_id(int gift_id) { + this.gift_id = gift_id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + } + + // 嵌套类:Data + public static class Data { + private int gift_hall_send_num; + private String user_name; + private String avatar; + private int id; + private int active_rank_hide; + private String create_time; + + // 同样地,这里省略getter和setter方法 + + public int getGift_hall_send_num() { + return gift_hall_send_num; + } + + public void setGift_hall_send_num(int gift_hall_send_num) { + this.gift_hall_send_num = gift_hall_send_num; + } + + public String getUser_name() { + return user_name; + } + + public void setUser_name(String user_name) { + this.user_name = user_name; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getActive_rank_hide() { + return active_rank_hide; + } + + public void setActive_rank_hide(int active_rank_hide) { + this.active_rank_hide = active_rank_hide; + } + + public String getCreate_time() { + return create_time; + } + + public void setCreate_time(String create_time) { + this.create_time = create_time; + } + } +} diff --git a/common/src/main/java/com/yunbao/common/bean/GiftWallTab2Bean.java b/common/src/main/java/com/yunbao/common/bean/GiftWallTab2Bean.java new file mode 100644 index 000000000..f46f12e83 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/bean/GiftWallTab2Bean.java @@ -0,0 +1,312 @@ +package com.yunbao.common.bean; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class GiftWallTab2Bean extends BaseModel{ + + + @SerializedName("illuminate_data") + private IlluminateData illuminateData; + @SerializedName("gift_hall_start_date") + private String gift_hall_start_date; + @SerializedName("gift_hall_end_date") + private String gift_hall_end_date; + + public GiftWallTab2Bean() { + } + + public IlluminateData getIlluminateData() { + return illuminateData; + } + + public void setIlluminateData(IlluminateData illuminateData) { + this.illuminateData = illuminateData; + } + + public String getGift_hall_start_date() { + return gift_hall_start_date; + } + + public void setGift_hall_start_date(String gift_hall_start_date) { + this.gift_hall_start_date = gift_hall_start_date; + } + + public String getGift_hall_end_date() { + return gift_hall_end_date; + } + + public void setGift_hall_end_date(String gift_hall_end_date) { + this.gift_hall_end_date = gift_hall_end_date; + } + + public static class IlluminateData{ + @SerializedName("week_start_data") + private List weekStartData; + @SerializedName("gift_data") + private List giftData; + + public IlluminateData() { + } + + public List getWeekStartData() { + return weekStartData; + } + + public void setWeekStartData(List weekStartData) { + this.weekStartData = weekStartData; + } + + public List getGiftData() { + return giftData; + } + + public void setGiftData(List giftData) { + this.giftData = giftData; + } + } + public static class Gift{ + @SerializedName("gift_id") + public int giftId; + + @SerializedName("sendtype") + public int sendType; + + @SerializedName("gift_name") + public String giftName; + + @SerializedName("need_coin") + public int needCoin; + + @SerializedName("gift_icon") + public String giftIcon; + + @SerializedName("week_star_level") + public int weekStarLevel; + + @SerializedName("illuminate_num") + public int illuminateNum; + + @SerializedName("gift_hall_type") + public int giftHallType; + + @SerializedName("gift_name_en") + public String giftNameEn; + + @SerializedName("gift_hall_start") + public String giftHallStart; + + @SerializedName("gift_hall_end") + public String giftHallEnd; + + @SerializedName("naming_live_id") + public int namingLiveId; + + @SerializedName("naming_live_nicename") + public String namingLiveNicename; + + @SerializedName("gift_hall_send_num") + public int giftHallSendNum; + + @SerializedName("naming_live_avatar") + public String namingLiveAvatar; + + @SerializedName("naming_live_active_rank_hide") + public int namingLiveActiveRankHide; + + @SerializedName("illuminate_status") + public int illuminateStatus; + + @SerializedName("naming_user_id") + public int namingUserId; + + @SerializedName("naming_user_nicename") + public String namingUserNicename; + + @SerializedName("naming_user_avatar") + public String namingUserAvatar; + + @SerializedName("naming_user_active_rank_hide") + public int namingUserActiveRankHide; + + @SerializedName("needcoin_total") + public int needCoinTotal; + + public int getGiftId() { + return giftId; + } + + public void setGiftId(int giftId) { + this.giftId = giftId; + } + + public int getSendType() { + return sendType; + } + + public void setSendType(int sendType) { + this.sendType = sendType; + } + + public String getGiftName() { + return giftName; + } + + public void setGiftName(String giftName) { + this.giftName = giftName; + } + + public int getNeedCoin() { + return needCoin; + } + + public void setNeedCoin(int needCoin) { + this.needCoin = needCoin; + } + + public String getGiftIcon() { + return giftIcon; + } + + public void setGiftIcon(String giftIcon) { + this.giftIcon = giftIcon; + } + + public int getWeekStarLevel() { + return weekStarLevel; + } + + public void setWeekStarLevel(int weekStarLevel) { + this.weekStarLevel = weekStarLevel; + } + + public int getIlluminateNum() { + return illuminateNum; + } + + public void setIlluminateNum(int illuminateNum) { + this.illuminateNum = illuminateNum; + } + + public int getGiftHallType() { + return giftHallType; + } + + public void setGiftHallType(int giftHallType) { + this.giftHallType = giftHallType; + } + + public String getGiftNameEn() { + return giftNameEn; + } + + public void setGiftNameEn(String giftNameEn) { + this.giftNameEn = giftNameEn; + } + + public String getGiftHallStart() { + return giftHallStart; + } + + public void setGiftHallStart(String giftHallStart) { + this.giftHallStart = giftHallStart; + } + + public String getGiftHallEnd() { + return giftHallEnd; + } + + public void setGiftHallEnd(String giftHallEnd) { + this.giftHallEnd = giftHallEnd; + } + + public int getNamingLiveId() { + return namingLiveId; + } + + public void setNamingLiveId(int namingLiveId) { + this.namingLiveId = namingLiveId; + } + + public String getNamingLiveNicename() { + return namingLiveNicename; + } + + public void setNamingLiveNicename(String namingLiveNicename) { + this.namingLiveNicename = namingLiveNicename; + } + + public int getGiftHallSendNum() { + return giftHallSendNum; + } + + public void setGiftHallSendNum(int giftHallSendNum) { + this.giftHallSendNum = giftHallSendNum; + } + + public String getNamingLiveAvatar() { + return namingLiveAvatar; + } + + public void setNamingLiveAvatar(String namingLiveAvatar) { + this.namingLiveAvatar = namingLiveAvatar; + } + + public int getNamingLiveActiveRankHide() { + return namingLiveActiveRankHide; + } + + public void setNamingLiveActiveRankHide(int namingLiveActiveRankHide) { + this.namingLiveActiveRankHide = namingLiveActiveRankHide; + } + + public int getIlluminateStatus() { + return illuminateStatus; + } + + public void setIlluminateStatus(int illuminateStatus) { + this.illuminateStatus = illuminateStatus; + } + + public int getNamingUserId() { + return namingUserId; + } + + public void setNamingUserId(int namingUserId) { + this.namingUserId = namingUserId; + } + + public String getNamingUserNicename() { + return namingUserNicename; + } + + public void setNamingUserNicename(String namingUserNicename) { + this.namingUserNicename = namingUserNicename; + } + + public String getNamingUserAvatar() { + return namingUserAvatar; + } + + public void setNamingUserAvatar(String namingUserAvatar) { + this.namingUserAvatar = namingUserAvatar; + } + + public int getNamingUserActiveRankHide() { + return namingUserActiveRankHide; + } + + public void setNamingUserActiveRankHide(int namingUserActiveRankHide) { + this.namingUserActiveRankHide = namingUserActiveRankHide; + } + + public int getNeedCoinTotal() { + return needCoinTotal; + } + + public void setNeedCoinTotal(int needCoinTotal) { + this.needCoinTotal = needCoinTotal; + } + } +} diff --git a/common/src/main/java/com/yunbao/common/dialog/AbsDialogPopupWindow.java b/common/src/main/java/com/yunbao/common/dialog/AbsDialogPopupWindow.java index be1adad33..aa385d22f 100644 --- a/common/src/main/java/com/yunbao/common/dialog/AbsDialogPopupWindow.java +++ b/common/src/main/java/com/yunbao/common/dialog/AbsDialogPopupWindow.java @@ -6,6 +6,11 @@ import androidx.annotation.NonNull; import com.lxj.xpopup.XPopup; import com.lxj.xpopup.core.BottomPopupView; +import com.yunbao.common.event.ClosePopupDialogEvent; +import com.yunbao.common.utils.Bus; + +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * 底部弹窗 @@ -16,12 +21,20 @@ public abstract class AbsDialogPopupWindow extends BottomPopupView { public AbsDialogPopupWindow(@NonNull Context context) { super(context); this.mContext = context; + Bus.getOn(this); + } + + @Override + public void dismiss() { + super.dismiss(); + Bus.getOff(this); } /** * 参考配置 */ public abstract void buildDialog(XPopup.Builder builder); + public abstract int bindLayoutId(); @Override @@ -36,4 +49,16 @@ public abstract class AbsDialogPopupWindow extends BottomPopupView { buildDialog(builder); builder.asCustom(this).show(); } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onWishSendGift(ClosePopupDialogEvent bean) { + if(bean.getId()==null) { + dismiss(); + return; + } + if(bean.getId().equals(getTag())) { + dismiss(); + } + } + } diff --git a/common/src/main/java/com/yunbao/common/dialog/GiftWallDialog.java b/common/src/main/java/com/yunbao/common/dialog/GiftWallDialog.java index 099fff9ee..45482b1ea 100644 --- a/common/src/main/java/com/yunbao/common/dialog/GiftWallDialog.java +++ b/common/src/main/java/com/yunbao/common/dialog/GiftWallDialog.java @@ -29,6 +29,7 @@ import com.yunbao.common.views.weight.ViewClicksAntiShake; import java.util.ArrayList; import java.util.List; +import java.util.Locale; /** * 礼物墙 @@ -46,9 +47,15 @@ public class GiftWallDialog extends AbsDialogPopupWindow { private List fragments = new ArrayList<>(); private boolean isFullWindows; + String toUserId; + String userName; + boolean isAnchor; - public GiftWallDialog(@NonNull Context context) { + public GiftWallDialog(@NonNull Context context, String toUserId, String userName, boolean isAnchor) { super(context); + this.toUserId = toUserId; + this.isAnchor = isAnchor; + this.userName = userName; } public GiftWallDialog setFullWindows(boolean fullWindows) { @@ -86,8 +93,13 @@ public class GiftWallDialog extends AbsDialogPopupWindow { mViewPager = findViewById(R.id.viewPager2); mIvTabsLayout = findViewById(R.id.tab_layout); - fragments.add(new GiftWallMainTab1Fragment()); - fragments.add(new GiftWallMainTab2Fragment()); + mTvUserName.setText(String.format(Locale.getDefault(), "%s%s", + userName, + WordUtil.isNewZh() ? "的禮物展館" : "'s Gift Hall" + )); + + fragments.add(new GiftWallMainTab1Fragment().setToUserId(toUserId).setAnchor(isAnchor).setLiveRoom(!isFullWindows)); + fragments.add(new GiftWallMainTab2Fragment().setToUserId(toUserId).setAnchor(isAnchor).setLiveRoom(!isFullWindows)); mViewPager.setAdapter(new FragmentStateAdapter((FragmentActivity) mContext) { @NonNull @Override @@ -101,6 +113,18 @@ public class GiftWallDialog extends AbsDialogPopupWindow { } }); mViewPager.setUserInputEnabled(false); + mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + fragments.get(position).updateData(); + if(position==0){ + mTvUserName.setTextColor(Color.parseColor("#6BCDFF")); + }else{ + mTvUserName.setTextColor(Color.parseColor("#FFCF94")); + } + } + }); ViewClicksAntiShake.clicksAntiShake(mTvTab1, () -> { @@ -113,7 +137,7 @@ public class GiftWallDialog extends AbsDialogPopupWindow { mTvTab2.setTextColor(Color.parseColor("#FFFFFF")); mTvTab2.setTextSize(14); mTvTab2.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); - mViewPager.setCurrentItem(0,false); + mViewPager.setCurrentItem(0, false); }); ViewClicksAntiShake.clicksAntiShake(mTvTab2, () -> { @@ -124,13 +148,13 @@ public class GiftWallDialog extends AbsDialogPopupWindow { mTvTab1.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); mTvTab2.setTextColor(Color.parseColor("#FFC593")); - if(WordUtil.isNewZh()) { + if (WordUtil.isNewZh()) { mTvTab2.setTextSize(16); - }else{ + } else { mTvTab2.setTextSize(14.5f); } mTvTab2.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); - mViewPager.setCurrentItem(1,false); + mViewPager.setCurrentItem(1, false); }); } @@ -141,11 +165,11 @@ public class GiftWallDialog extends AbsDialogPopupWindow { initView(); ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mIvBack.getLayoutParams(); if (isFullWindows) { - params.width=DpUtil.dp2px(20); + params.width = DpUtil.dp2px(20); mIvBack.setVisibility(View.VISIBLE); mIvBg.setScaleType(ImageView.ScaleType.CENTER_CROP); } else { - params.width=DpUtil.dp2px(1); + params.width = DpUtil.dp2px(1); mIvBack.setVisibility(View.INVISIBLE); mIvBg.setScaleType(ImageView.ScaleType.FIT_XY); } diff --git a/common/src/main/java/com/yunbao/common/dialog/GiftWallGiftInfoDialog.java b/common/src/main/java/com/yunbao/common/dialog/GiftWallGiftInfoDialog.java new file mode 100644 index 000000000..801756145 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/dialog/GiftWallGiftInfoDialog.java @@ -0,0 +1,392 @@ +package com.yunbao.common.dialog; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Color; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.lxj.xpopup.XPopup; +import com.makeramen.roundedimageview.RoundedImageView; +import com.opensource.svgaplayer.SVGADrawable; +import com.opensource.svgaplayer.SVGAImageView; +import com.opensource.svgaplayer.SVGAParser; +import com.opensource.svgaplayer.SVGAVideoEntity; +import com.yunbao.common.R; +import com.yunbao.common.adapter.GiftWallGiftInfoListItemAdapter; +import com.yunbao.common.bean.GiftWallInfoBean; +import com.yunbao.common.bean.JsWishBean; +import com.yunbao.common.custom.ItemDecoration; +import com.yunbao.common.event.ClosePopupDialogEvent; +import com.yunbao.common.glide.ImgLoader; +import com.yunbao.common.http.base.HttpCallback; +import com.yunbao.common.http.live.LiveNetManager; +import com.yunbao.common.interfaces.OnItemClickListener; +import com.yunbao.common.manager.IMLoginManager; +import com.yunbao.common.utils.Bus; +import com.yunbao.common.utils.ScreenDimenUtil; +import com.yunbao.common.utils.WordUtil; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +import java.util.Locale; + +public class GiftWallGiftInfoDialog extends AbsDialogPopupWindow { + private boolean isFullWindows; + private ImageView mIvBg; + private ImageView mIvBack; + TextView giftName; + ImageView gift; + SVGAImageView gift_bg; + TextView diamond_text; + TextView gift_tv_progress; + TextView gift_tv_max; + ProgressBar gift_progress; + Button gift_btn; + Button tab1, tab2; + ImageView tips_timer; + TextView tv_list_title; + RoundedImageView avatar; + TextView user_name; + TextView send_num; + TextView btn_one; + Button btn_one_tips; + Button btn_lighten; + View tab_layout; + View bottom_layout; + + GiftWallGiftInfoListItemAdapter adapter; + RecyclerView recyclerView; + + String giftId; + String toUserId; + boolean isAnchor; + int gift_hall_type = 1; + int list_type = 1; + private boolean isLiveRoom; + String time; + private boolean isStar; + + + public GiftWallGiftInfoDialog(Context context, String giftId, String toUserId, boolean isAnchor) { + super(context); + this.giftId = giftId; + this.toUserId = toUserId; + this.isAnchor = isAnchor; + } + + public GiftWallGiftInfoDialog setFullWindows(boolean fullWindows) { + isFullWindows = fullWindows; + return this; + } + + + public GiftWallGiftInfoDialog setLiveRoom(boolean isLiveRoom) { + this.isLiveRoom = isLiveRoom; + return this; + } + + public GiftWallGiftInfoDialog setStar(boolean isStar) { + this.isStar = isStar; + return this; + } + + @Override + protected int getPopupHeight() { + if (isFullWindows) { + return super.getPopupHeight(); + } + int screenHeight = ScreenDimenUtil.getInstance().getScreenHeight(); + return (int) (screenHeight * 0.8); + } + + @Override + public void buildDialog(XPopup.Builder builder) { + + } + + @Override + public int bindLayoutId() { + return R.layout.dialog_gift_wall_gift_info; + } + + @Override + protected void onCreate() { + super.onCreate(); + initView(); + initData(); + } + + void initView() { + mIvBg = findViewById(R.id.iv_root_bg); + mIvBack = findViewById(R.id.iv_back); + giftName = findViewById(R.id.gift_name); + gift = findViewById(R.id.gift); + diamond_text = findViewById(R.id.diamond_text); + gift_tv_progress = findViewById(R.id.gift_tv_progress); + gift_tv_max = findViewById(R.id.gift_tv_max); + gift_progress = findViewById(R.id.gift_progress); + gift_btn = findViewById(R.id.gift_btn); + tab1 = findViewById(R.id.tab1); + tab2 = findViewById(R.id.tab2); + tips_timer = findViewById(R.id.tips_timer); + tv_list_title = findViewById(R.id.tv_list_title); + avatar = findViewById(R.id.bottom_avatar); + user_name = findViewById(R.id.bottom_user_name); + send_num = findViewById(R.id.send_num); + btn_one = findViewById(R.id.btn_one); + btn_one_tips = findViewById(R.id.btn_one_tips); + gift_bg = findViewById(R.id.gift_bg); + btn_lighten = findViewById(R.id.btn_lighten); + recyclerView = findViewById(R.id.recyclerView); + tab_layout = findViewById(R.id.tab_layout); + bottom_layout = findViewById(R.id.bottom_layout); + adapter = new GiftWallGiftInfoListItemAdapter(); + recyclerView.setAdapter(adapter); + recyclerView.addItemDecoration(new ItemDecoration(mContext, 0x00000000, 0, 10)); + + if (isStar) { + tab2.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_title_star)); + ((TextView) findViewById(R.id.user_name)).setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_info_list_header_rename1)); + ((TextView) findViewById(R.id.tv_rename)).setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_info_list_header_rename_value1)); + tv_list_title.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_title_star)); + } else { + tab2.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_title_champion)); + tv_list_title.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_title_champion)); + ((TextView) findViewById(R.id.user_name)).setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_info_list_header_rename)); + ((TextView) findViewById(R.id.tv_rename)).setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_info_list_header_rename_value)); + } + btn_one.setVisibility(View.GONE); + btn_one_tips.setVisibility(View.GONE); + ViewClicksAntiShake.clicksAntiShake(tab1, () -> { + + btn_one.setVisibility(View.GONE); + btn_one_tips.setVisibility(View.GONE); + + tab1.setBackgroundResource(R.drawable.gift_wall_gift_info_list_btn_up); + tab1.setTextColor(Color.parseColor("#31326D")); + + tab2.setBackgroundResource(R.drawable.gift_wall_gift_info_list_btn_down); + tab2.setTextColor(Color.parseColor("#FFFFFF")); + list_type = 1; + initData(); + }); + ViewClicksAntiShake.clicksAntiShake(tab2, () -> { + btn_one.setVisibility(View.VISIBLE); + btn_one_tips.setVisibility(View.VISIBLE); + + tab2.setBackgroundResource(R.drawable.gift_wall_gift_info_list_btn_up); + tab2.setTextColor(Color.parseColor("#31326D")); + + tab1.setBackgroundResource(R.drawable.gift_wall_gift_info_list_btn_down); + tab1.setTextColor(Color.parseColor("#FFFFFF")); + list_type = 2; + initData(); + }); + XPopup.Builder builder = new XPopup.Builder(getContext()) + .atView(tips_timer); + builder.hasShadowBg(false); + tips_timer.setTag(builder); + ViewClicksAntiShake.clicksAntiShake(tips_timer, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + XPopup.Builder b = (XPopup.Builder) tips_timer.getTag(); + b.asCustom(new GiftWallMainTab1TipsDialog(mContext, new OnItemClickListener() { + @Override + public void onItemClick(Integer bean, int position) { + + } + }).setTime(time)).show(); + } + }); + ViewClicksAntiShake.clicksAntiShake(btn_one, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + Bus.get().post(new JsWishBean(giftId));//setUname==by + Bus.get().post(new ClosePopupDialogEvent()); + } + }); + ViewClicksAntiShake.clicksAntiShake(gift_btn, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + Bus.get().post(new JsWishBean(giftId));//setUname==by + Bus.get().post(new ClosePopupDialogEvent()); + } + }); + } + + void initData() { + if (isAnchor) { + LiveNetManager.get(mContext) + .liveGiftHallDetail(toUserId, giftId, gift_hall_type, list_type, new HttpCallback() { + @Override + public void onSuccess(GiftWallInfoBean data) { + initData(data); + } + + @Override + public void onError(String error) { + + } + }); + } else { + LiveNetManager.get(mContext) + .singleUserGiftHallDetail(toUserId, giftId, new HttpCallback() { + @Override + public void onSuccess(GiftWallInfoBean data) { + initData(data); + } + + @Override + public void onError(String error) { + + } + }); + } + + + } + + void initData(GiftWallInfoBean giftBean) { + giftName.setText(WordUtil.isNewZh() ? giftBean.getGift_info().getGiftname() : giftBean.getGift_info().getGiftname_en()); + ImgLoader.display(mContext, giftBean.getGift_info().getGifticon(), gift); + diamond_text.setText(String.format(Locale.getDefault(), "%d", giftBean.getGift_info().getNeedcoin())); + gift_tv_max.setText(String.format(Locale.getDefault(), "/%s", giftBean.getGift_info().getIlluminate_num())); + gift_tv_progress.setText(String.format(Locale.getDefault(), "%s", giftBean.getGift_info().getGift_hall_send_num())); + gift_progress.setMax(Integer.parseInt(giftBean.getGift_info().getGift_hall_send_num())); + gift_progress.setProgress(giftBean.getGift_info().getNeedcoin_total()); + if (giftBean.getGift_info().getIlluminate_status() == 1) { + gift_btn.setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_info_top_btn_continue)); + btn_lighten.setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_spinner_up)); + btn_lighten.setBackgroundResource(R.drawable.gift_wall_gift_info_lighten); + } else { + gift_btn.setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_info_top_btn_to)); + btn_lighten.setText(WordUtil.getNewString(R.string.dialog_gift_wall_list_spinner_down)); + btn_lighten.setBackgroundResource(R.drawable.gift_wall_gift_info_un_lighten); + } + if (isAnchor) { + tab_layout.setVisibility(View.VISIBLE); + bottom_layout.setVisibility(View.VISIBLE); + tv_list_title.setVisibility(View.GONE); + } else { + tab_layout.setVisibility(View.GONE); + bottom_layout.setVisibility(View.GONE); + tv_list_title.setVisibility(View.VISIBLE); + gift_btn.setVisibility(View.GONE); + } + if (!isLiveRoom) { + gift_btn.setEnabled(false); + } + adapter.setData(giftBean.getData()); + time = (WordUtil.isNewZh() ? "榜單結算時間" : "Settlement time") + giftBean.getGift_info().getGift_hall_start() + " - " + giftBean.getGift_info().getGift_hall_end(); + ImgLoader.display(mContext, IMLoginManager.get(mContext).getUserInfo().getAvatar(), avatar); + user_name.setText(IMLoginManager.get(mContext).getUserInfo().getUserNicename()); + send_num.setText(String.format(Locale.getDefault(), "%s", giftBean.getGift_info().getGift_hall_send_num())); + + String tmp = ""; + if (giftBean.getGift_info().getIlluminate_status() == 0) {//未點亮 + btn_one.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_bottom_btn_one_light)); + tmp = String.format(Locale.getDefault(), "%s", (giftBean.getGift_info().getIlluminate_num() - Integer.parseInt(giftBean.getGift_info().getGift_hall_send_num()))); + setTips(tmp); + } else {//冠名/摘星 + if (isStar) { + loadStar(giftBean); + } else { + loadChampion(giftBean); + } + } + + + initAnim(); + } + + private void setTips(String tmp) { + btn_one_tips.setText(String.format(Locale.getDefault(), "%s%s%s" + , WordUtil.isNewZh() ? "需" : "Need", + tmp, + WordUtil.isNewZh() ? "個" : "")); + } + + private void loadChampion(GiftWallInfoBean giftBean) { + int tmp = 0; + int mySend = Integer.parseInt(giftBean.getGift_info().getGift_hall_send_num()); + if (giftBean.getData() != null && !giftBean.getData().isEmpty()) { + tmp = giftBean.getData().get(0).getGift_hall_send_num() - Integer.parseInt(giftBean.getGift_info().getGift_hall_send_num()) + 1; + } + if (mySend > tmp) { + btn_one.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_bottom_btn_one_champion_get)); + btn_one_tips.setVisibility(View.GONE); + } else { + btn_one.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_bottom_btn_one_champion)); + setTips(String.valueOf(tmp + 1)); + } + } + + private void loadStar(GiftWallInfoBean giftBean) { + int tmp = 0; + int mySend = Integer.parseInt(giftBean.getGift_info().getGift_hall_send_num()); + if (giftBean.getData() != null && !giftBean.getData().isEmpty()) { + tmp = giftBean.getData().get(0).getGift_hall_send_num() - Integer.parseInt(giftBean.getGift_info().getGift_hall_send_num()) + 1; + } + if (mySend > tmp) { + btn_one.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_bottom_btn_one_star_get)); + btn_one_tips.setVisibility(View.GONE); + } else { + btn_one.setText(WordUtil.getNewString(R.string.dialog_gift_wall_gfit_info_list_bottom_btn_one_star)); + setTips(String.valueOf(tmp + 1)); + } + } + + void initAnim() { + if (gift.getTag() != null) { + return; + } + // 创建一个向上移动的动画 + ObjectAnimator upAnimator = ObjectAnimator.ofFloat(gift, "translationY", 0f, -10f); // 假设 10f 是你想要移动的距离(注意:这里使用的是像素值,而不是 dp) + upAnimator.setDuration(1000); // 设置动画时长为 1000 毫秒(即 1 秒) + // 创建一个向下移动的动画 + ObjectAnimator downAnimator = ObjectAnimator.ofFloat(gift, "translationY", -10f, 0f); + downAnimator.setDuration(1000); // 同样设置动画时长为 1 秒 + // 设置动画监听器以在向上动画结束后开始向下动画(如果需要的话) + upAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + // 在这里开始向下动画 + downAnimator.start(); + } + }); + downAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + gift.postDelayed(upAnimator::start, 1000); + } + }); + // 开始向上动画 + upAnimator.start(); + gift.setTag("start"); + + new SVGAParser(getContext()).decodeFromAssets("gift_wall_gift_info_light.svga", new SVGAParser.ParseCompletion() { + @Override + public void onComplete(@NonNull SVGAVideoEntity videoItem) { + gift_bg.setImageDrawable(new SVGADrawable(videoItem)); + gift_bg.setLoops(0); + gift_bg.startAnimation(); + } + + @Override + public void onError() { + System.err.println("-------------SVGA报错了"); + } + }, null); + } + +} diff --git a/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab1List2SpinnerDialog.java b/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab1List2SpinnerDialog.java new file mode 100644 index 000000000..b2d07a45c --- /dev/null +++ b/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab1List2SpinnerDialog.java @@ -0,0 +1,54 @@ +package com.yunbao.common.dialog; + +import android.content.Context; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.lxj.xpopup.core.AttachPopupView; +import com.yunbao.common.R; +import com.yunbao.common.interfaces.OnItemClickListener; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +public class GiftWallMainTab1List2SpinnerDialog extends AttachPopupView { + TextView all, up, down; + OnItemClickListener listener; + + public GiftWallMainTab1List2SpinnerDialog(@NonNull Context context, OnItemClickListener listener) { + super(context); + this.listener = listener; + } + + @Override + protected void onDismiss() { + super.onDismiss(); + listener.onItemClick(-1, 0); + } + + @Override + protected int getImplLayoutId() { + return R.layout.dialog_gift_wall_main_tab1_list2_spinner; + } + + @Override + protected void onCreate() { + super.onCreate(); + all = findViewById(R.id.spinner_all); + up = findViewById(R.id.spinner_up); + down = findViewById(R.id.spinner_down); + ViewClicksAntiShake.clicksAntiShake(all, () -> { + listener.onItemClick(0, 0); + dismiss(); + }); + + ViewClicksAntiShake.clicksAntiShake(up, () -> { + listener.onItemClick(1, 0); + dismiss(); + }); + + ViewClicksAntiShake.clicksAntiShake(down, () -> { + listener.onItemClick(2, 0); + dismiss(); + }); + } +} diff --git a/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab1TipsDialog.java b/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab1TipsDialog.java new file mode 100644 index 000000000..72a1794c5 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab1TipsDialog.java @@ -0,0 +1,48 @@ +package com.yunbao.common.dialog; + +import android.content.Context; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.lxj.xpopup.core.AttachPopupView; +import com.yunbao.common.R; +import com.yunbao.common.interfaces.OnItemClickListener; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +public class GiftWallMainTab1TipsDialog extends AttachPopupView { + TextView tips; + OnItemClickListener listener; + String time; + + public GiftWallMainTab1TipsDialog(@NonNull Context context, OnItemClickListener listener) { + super(context); + this.listener = listener; + } + + public GiftWallMainTab1TipsDialog setTime(String time) { + this.time = time; + return this; + } + + @Override + protected void onDismiss() { + super.onDismiss(); + listener.onItemClick(-1, 0); + } + + @Override + protected int getImplLayoutId() { + return R.layout.dialog_gift_wall_main_tab1_tips; + } + + @Override + protected void onCreate() { + super.onCreate(); + tips = findViewById(R.id.tips); + tips.setText(time); + ViewClicksAntiShake.clicksAntiShake(tips, () -> { + listener.onItemClick(0, 0); + }); + } +} diff --git a/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab2ClassicInfoDialog.java b/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab2ClassicInfoDialog.java new file mode 100644 index 000000000..a0a6bfdd8 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/dialog/GiftWallMainTab2ClassicInfoDialog.java @@ -0,0 +1,116 @@ +package com.yunbao.common.dialog; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.lxj.xpopup.XPopup; +import com.yunbao.common.R; +import com.yunbao.common.bean.GiftWallTab2Bean; +import com.yunbao.common.fragment.BaseFragment; +import com.yunbao.common.fragment.GiftWallMainTab1Fragment; +import com.yunbao.common.fragment.GiftWallMainTab2Fragment; +import com.yunbao.common.utils.DpUtil; +import com.yunbao.common.utils.ScreenDimenUtil; +import com.yunbao.common.utils.WordUtil; +import com.yunbao.common.views.CustomEllipsizeTextView; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class GiftWallMainTab2ClassicInfoDialog extends AbsDialogPopupWindow { + + private ImageView mIvBg; + private ImageView mIvTips; + private ImageView mIvBack; + private CustomEllipsizeTextView mTvUserName; + private View mAchievement; + private TextView mTvTab1, mTvTab2; + private ViewPager2 mViewPager; + private LinearLayout mIvTabsLayout; + private List fragments = new ArrayList<>(); + + private boolean isFullWindows; + GiftWallTab2Bean.Gift gift; + String userName; + boolean isAnchor; + + public GiftWallMainTab2ClassicInfoDialog(@NonNull Context context, GiftWallTab2Bean.Gift gift,boolean isAnchor) { + super(context); + this.gift = gift; + this.isAnchor = isAnchor; + } + + public GiftWallMainTab2ClassicInfoDialog setFullWindows(boolean fullWindows) { + isFullWindows = fullWindows; + return this; + } + + @Override + public void buildDialog(XPopup.Builder builder) { + + } + + @Override + public int bindLayoutId() { + return R.layout.dialog_gift_wall_tab2_classic_info; + } + + @Override + protected int getPopupHeight() { + if (isFullWindows) { + return super.getPopupHeight(); + } + int screenHeight = ScreenDimenUtil.getInstance().getScreenHeight(); + return (int) (screenHeight * 0.8); + } + + void initView() { + mIvBg = findViewById(R.id.iv_root_bg); + mIvTips = findViewById(R.id.v_tips); + mIvBack = findViewById(R.id.iv_back); + mTvUserName = findViewById(R.id.user_name); + mAchievement = findViewById(R.id.v_achievement); + mTvTab1 = findViewById(R.id.tab1); + mTvTab2 = findViewById(R.id.tab2); + mViewPager = findViewById(R.id.viewPager2); + mIvTabsLayout = findViewById(R.id.tab_layout); + + mTvUserName.setText(String.format(Locale.getDefault(), "%s%s", + userName, + WordUtil.isNewZh() ? "的禮物展館" : "'s Gift Hall" + )); + + } + + @Override + protected void onCreate() { + super.onCreate(); + initView(); + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mIvBack.getLayoutParams(); + if (isFullWindows) { + params.width = DpUtil.dp2px(20); + mIvBack.setVisibility(View.VISIBLE); + mIvBg.setScaleType(ImageView.ScaleType.CENTER_CROP); + } else { + params.width = DpUtil.dp2px(1); + mIvBack.setVisibility(View.INVISIBLE); + mIvBg.setScaleType(ImageView.ScaleType.FIT_XY); + } + mIvBack.setLayoutParams(params); + + } +} diff --git a/common/src/main/java/com/yunbao/common/event/ClosePopupDialogEvent.java b/common/src/main/java/com/yunbao/common/event/ClosePopupDialogEvent.java new file mode 100644 index 000000000..7dab53f00 --- /dev/null +++ b/common/src/main/java/com/yunbao/common/event/ClosePopupDialogEvent.java @@ -0,0 +1,22 @@ +package com.yunbao.common.event; + +import com.yunbao.common.bean.BaseModel; + +public class ClosePopupDialogEvent extends BaseModel { + public Object id=null; + + public ClosePopupDialogEvent(Object id) { + this.id = id; + } + + public Object getId() { + return id; + } + + public void setId(Object id) { + this.id = id; + } + + public ClosePopupDialogEvent() { + } +} diff --git a/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab1Fragment.java b/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab1Fragment.java index 46ace5f33..516ffde17 100644 --- a/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab1Fragment.java +++ b/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab1Fragment.java @@ -1,20 +1,58 @@ package com.yunbao.common.fragment; import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Shader; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.lxj.xpopup.XPopup; +import com.opensource.svgaplayer.SVGAParser; +import com.opensource.svgaplayer.SVGAVideoEntity; +import com.yunbao.common.R; +import com.yunbao.common.adapter.GiftWallMainTab1List2Adapter; +import com.yunbao.common.bean.GiftWallBean; +import com.yunbao.common.custom.ItemDecoration; +import com.yunbao.common.dialog.GiftWallMainTab1List2SpinnerDialog; +import com.yunbao.common.http.base.HttpCallback; +import com.yunbao.common.http.live.LiveNetManager; +import com.yunbao.common.interfaces.OnItemClickListener; +import com.yunbao.common.manager.IMLoginManager; +import com.yunbao.common.utils.DpUtil; +import com.yunbao.common.utils.WordUtil; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +import java.util.Locale; + public class GiftWallMainTab1Fragment extends BaseFragment { + TextView classicGiftsTitle, starGiftsTitle; + TextView classicGiftsNumber, starGiftsNumber; + RecyclerView recyclerView1, recyclerView2; + View spinner; + TextView spinnerText; + ImageView spinnerImage; + + GiftWallMainTab1List2Adapter list1Adapter, list2Adapter; + + int screen; + String toUserId; + boolean isAnchor; + boolean isLiveRoom; + SVGAVideoEntity drawable; + View classic_layout, star_layout; + TextView show_time; + View show_time_layout; + @Override public View createView(LayoutInflater inflater, ViewGroup container) { - TextView textView = new TextView(getActivity()); - textView.setText("第一页"); - textView.setTextColor(Color.WHITE); - textView.setTextSize(24); - return textView; + return inflater.inflate(R.layout.fragment_gift_wall_man_tab_1, container, false); } @Override @@ -22,9 +60,172 @@ public class GiftWallMainTab1Fragment extends BaseFragment { } + public GiftWallMainTab1Fragment setToUserId(String toUserId) { + this.toUserId = toUserId; + return this; + } + + public GiftWallMainTab1Fragment setAnchor(boolean anchor) { + isAnchor = anchor; + return this; + } + + public GiftWallMainTab1Fragment setLiveRoom(boolean liveRoom) { + isLiveRoom = liveRoom; + return this; + } + + @Override + public void updateData() { + super.updateData(); + list1Adapter.notifyDataSetChanged(); + list2Adapter.notifyDataSetChanged(); + + } + + private void initData() { + if (isAnchor) { + LiveNetManager.get(getContext()) + .liveGiftHall(toUserId, screen, new HttpCallback() { + @Override + public void onSuccess(GiftWallBean data) { + initData(data); + } + + @Override + public void onError(String error) { + + } + }); + } else { + LiveNetManager.get(getContext()) + .singleUserGiftHall(toUserId, screen, new HttpCallback() { + @Override + public void onSuccess(GiftWallBean data) { + initData(data); + } + + @Override + public void onError(String error) { + + } + }); + } + } + + private void initData(GiftWallBean bean) { + list1Adapter.setDrawable(drawable); + list1Adapter.setList(bean.getIlluminate_data().getWeek_star_data()); + list2Adapter.setList(bean.getIlluminate_data().getGift_data()); + list1Adapter.setLiveRoom(isLiveRoom); + list2Adapter.setLiveRoom(isLiveRoom); + + starGiftsNumber.setText(String.format(Locale.getDefault(), "%s%d/%d", + WordUtil.getNewString(R.string.dialog_gift_wall_list_spinner_up), + bean.getIlluminate_data().getWeek_gift_illuminate_num(), + bean.getIlluminate_data().getWeek_star_gift_num() + )); + classicGiftsNumber.setText(String.format(Locale.getDefault(), "%s%d/%d", + WordUtil.getNewString(R.string.dialog_gift_wall_list_spinner_up), + bean.getIlluminate_data().getGift_illuminate_num(), + bean.getIlluminate_data().getGift_num() + )); + if (bean.getIlluminate_data().getWeek_star_data() != null) { + show_time.setText(String.format(Locale.getDefault(), "%s - %s", + bean.getIlluminate_data().getGift_hall_start_date(), + bean.getIlluminate_data().getGift_hall_end_date())); + } + } + @Override protected void initViews(Bundle savedInstanceState, View contentView) { + classicGiftsTitle = contentView.findViewById(R.id.classic_gifts_title); + classicGiftsNumber = contentView.findViewById(R.id.classic_gifts_number); + recyclerView1 = contentView.findViewById(R.id.recyclerView1); + recyclerView2 = contentView.findViewById(R.id.recyclerView2); + starGiftsTitle = contentView.findViewById(R.id.star_gifts_title); + starGiftsNumber = contentView.findViewById(R.id.star_gifts_number); + spinner = contentView.findViewById(R.id.classic_gift_spinner); + spinnerText = contentView.findViewById(R.id.classic_gift_spinner_text); + spinnerImage = contentView.findViewById(R.id.classic_gift_spinner_ic); + classic_layout = contentView.findViewById(R.id.classic_layout); + star_layout = contentView.findViewById(R.id.star_layout); + show_time = contentView.findViewById(R.id.show_time); + show_time_layout = contentView.findViewById(R.id.show_time_layout); + list1Adapter = new GiftWallMainTab1List2Adapter(getContext()); + list2Adapter = new GiftWallMainTab1List2Adapter(getContext()); + + list1Adapter.setToUid(toUserId); + list2Adapter.setToUid(toUserId); + list1Adapter.setAnchor(isAnchor); + list2Adapter.setAnchor(isAnchor); + list1Adapter.setStar(true); + + recyclerView1.setAdapter(list1Adapter); + recyclerView2.setAdapter(list2Adapter); + + recyclerView1.addItemDecoration(new ItemDecoration(getContext(), 0x00000000, DpUtil.dp2px(10), 1)); + recyclerView2.addItemDecoration(new ItemDecoration(getContext(), 0x00000000, DpUtil.dp2px(2), 1)); + + starGiftsTitle.getPaint().setShader(new LinearGradient(0, 0, 0, classicGiftsTitle.getPaint().getTextSize(), + Color.parseColor("#FEE8C6"), Color.parseColor("#FFD5A3"), Shader.TileMode.CLAMP)); + + classicGiftsTitle.getPaint().setShader(new LinearGradient(0, 0, 0, classicGiftsTitle.getPaint().getTextSize(), + Color.parseColor("#A0E9FF"), Color.parseColor("#72D1FF"), Shader.TileMode.CLAMP)); + XPopup.Builder builder = new XPopup.Builder(getContext()) + .atView(spinner); + spinner.setTag(builder); + + if (!isAnchor) { + show_time_layout.setVisibility(View.GONE); + star_layout.setVisibility(View.GONE); + } + if (!isLiveRoom) { + + } + + + ViewClicksAntiShake.clicksAntiShake(spinner, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + spinnerImage.setRotation(180); + ((XPopup.Builder) spinner.getTag()).asCustom(new GiftWallMainTab1List2SpinnerDialog(getContext(), new OnItemClickListener() { + @Override + public void onItemClick(Integer bean, int position) { + if (bean > -1) { + screen = bean; + initData(); + } + switch (bean) { + case 0: + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_all); + break; + case 1: + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_up); + break; + case 2: + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_down); + break; + } + spinnerImage.setRotation(0); + } + })).show(); + + } + }); + new SVGAParser(getContext()).decodeFromAssets("gift_wall_light_up.svga", new SVGAParser.ParseCompletion() { + @Override + public void onComplete(@NonNull SVGAVideoEntity videoItem) { + GiftWallMainTab1Fragment.this.drawable = videoItem; + initData(); + } + + @Override + public void onError() { + System.err.println("-------------SVGA报错了"); + } + }, null); } @Override diff --git a/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab2Fragment.java b/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab2Fragment.java index 9c4b7f12d..7cf250cb6 100644 --- a/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab2Fragment.java +++ b/common/src/main/java/com/yunbao/common/fragment/GiftWallMainTab2Fragment.java @@ -1,20 +1,75 @@ package com.yunbao.common.fragment; import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Shader; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + +import com.angcyo.tablayout.DslTabLayout; +import com.angcyo.tablayout.DslTabLayoutConfig; +import com.lxj.xpopup.XPopup; +import com.yunbao.common.R; +import com.yunbao.common.adapter.GiftWallMainTab2ListAdapter; +import com.yunbao.common.bean.GiftWallTab2Bean; +import com.yunbao.common.custom.ItemDecoration; +import com.yunbao.common.dialog.GiftWallMainTab1List2SpinnerDialog; +import com.yunbao.common.http.base.HttpCallback; +import com.yunbao.common.http.live.LiveNetManager; +import com.yunbao.common.interfaces.OnItemClickListener; +import com.yunbao.common.utils.DpUtil; +import com.yunbao.common.views.weight.ViewClicksAntiShake; + +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function4; + public class GiftWallMainTab2Fragment extends BaseFragment { + TextView classicGiftsTitle, starGiftsTitle; + RecyclerView recyclerView1, recyclerView2; + View spinner; + TextView spinnerText; + ImageView spinnerImage; + GiftWallMainTab2ListAdapter list1Adapter, list2Adapter; + TextView show_time; + DslTabLayout tabLayout; + TextView dslTab1, dslTab2; + + + int screen; + int type = 1; + String toUserId; + boolean isAnchor; + private boolean isLiveRoom; + + public GiftWallMainTab2Fragment setToUserId(String toUserId) { + this.toUserId = toUserId; + return this; + } + + public GiftWallMainTab2Fragment setAnchor(boolean anchor) { + isAnchor = anchor; + return this; + } + + public GiftWallMainTab2Fragment setLiveRoom(boolean liveRoom) { + isLiveRoom = liveRoom; + return this; + } + @Override public View createView(LayoutInflater inflater, ViewGroup container) { - TextView textView = new TextView(getActivity()); - textView.setText("第二页"); - textView.setTextColor(Color.WHITE); - textView.setTextSize(24); - return textView; + return inflater.inflate(R.layout.fragment_gift_wall_man_tab_2, container, false); } @Override @@ -24,7 +79,156 @@ public class GiftWallMainTab2Fragment extends BaseFragment { @Override protected void initViews(Bundle savedInstanceState, View contentView) { + classicGiftsTitle = contentView.findViewById(R.id.classic_gifts_title); + recyclerView1 = contentView.findViewById(R.id.recyclerView1); + recyclerView2 = contentView.findViewById(R.id.recyclerView2); + starGiftsTitle = contentView.findViewById(R.id.star_gifts_title); + spinner = contentView.findViewById(R.id.classic_gift_spinner); + spinnerText = contentView.findViewById(R.id.classic_gift_spinner_text); + spinnerImage = contentView.findViewById(R.id.classic_gift_spinner_ic); + show_time = contentView.findViewById(R.id.show_time); + tabLayout = findViewById(R.id.tab_layout); + dslTab1 = findViewById(R.id.dsl_tab1); + dslTab2 = findViewById(R.id.dsl_tab2); + list1Adapter = new GiftWallMainTab2ListAdapter(getContext()); + list2Adapter = new GiftWallMainTab2ListAdapter(getContext()); + + list1Adapter.setToUid(toUserId); + list2Adapter.setToUid(toUserId); + list1Adapter.setAnchor(isAnchor); + list2Adapter.setAnchor(isAnchor); + list1Adapter.setStar(true); + + recyclerView1.setAdapter(list1Adapter); + recyclerView2.setAdapter(list2Adapter); + + + recyclerView1.addItemDecoration(new ItemDecoration(getContext(), 0x00000000, DpUtil.dp2px(2), 1)); + recyclerView2.addItemDecoration(new ItemDecoration(getContext(), 0x00000000, DpUtil.dp2px(2), 1)); + + starGiftsTitle.getPaint().setShader(new LinearGradient(0, 0, 0, classicGiftsTitle.getPaint().getTextSize(), + Color.parseColor("#FEE8C6"), Color.parseColor("#FFD5A3"), Shader.TileMode.CLAMP)); + + classicGiftsTitle.getPaint().setShader(new LinearGradient(0, 0, 0, classicGiftsTitle.getPaint().getTextSize(), + Color.parseColor("#A0E9FF"), Color.parseColor("#72D1FF"), Shader.TileMode.CLAMP)); + + setDslTabColor(dslTab1, dslTab2); + + tabLayout.configTabLayoutConfig(new Function1() { + @Override + public Unit invoke(DslTabLayoutConfig dslTabLayoutConfig) { + dslTabLayoutConfig.setOnSelectItemView(new Function4() { + @Override + public Boolean invoke(View itemView, Integer index, Boolean select, Boolean fromUser) { + //当需要选中itemView时, 触发的回调. + //返回true, 拦截库中的默认处理. + setDslTabColor(index == 1 ? dslTab1 : dslTab2, index == 0 ? dslTab1 : dslTab2); + type = (index + 1); + screen=0; + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_all); + initData(); + return false; + } + }); + dslTabLayoutConfig.setOnSelectViewChange(new Function4, Boolean, Boolean, Unit>() { + @Override + public Unit invoke(View fromView, List selectViewList, Boolean select, Boolean fromUser) { + //fromView 表示之前选中的view, 多选模式不支持. + //selectViewList 表示目前选中view的集合, 单选模式列表里面只有一个,可以使用selectViewList.get(0)获取. + return null; + } + }); + dslTabLayoutConfig.setOnSelectIndexChange(new Function4, Boolean, Boolean, Unit>() { + @Override + public Unit invoke(Integer fromIndex, List selectIndexList, Boolean select, Boolean fromUser) { + //参考setOnSelectViewChange + //只不过对象从view,变成了view在ViewGroup中的索引 + return null; + } + }); + return null; + } + }); + + XPopup.Builder builder = new XPopup.Builder(getContext()) + .atView(spinner); + spinner.setTag(builder); + + ViewClicksAntiShake.clicksAntiShake(spinner, new ViewClicksAntiShake.ViewClicksCallBack() { + @Override + public void onViewClicks() { + spinnerImage.setRotation(180); + ((XPopup.Builder) spinner.getTag()).asCustom(new GiftWallMainTab1List2SpinnerDialog(getContext(), new OnItemClickListener() { + @Override + public void onItemClick(Integer bean, int position) { + if (bean > -1) { + screen = bean; + initData(); + } + switch (bean) { + case 0: + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_all); + break; + case 1: + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_up); + break; + case 2: + spinnerText.setText(R.string.dialog_gift_wall_list_spinner_down); + break; + } + spinnerImage.setRotation(0); + } + })).show(); + + } + }); + initData(); + } + + void setDslTabColor(TextView select, TextView unSelect) { + select.getPaint().setShader(new LinearGradient(0, 0, 0, select.getPaint().getTextSize(), + Color.parseColor("#FEE0B6"), Color.parseColor("#FFCF95"), Shader.TileMode.CLAMP)); + unSelect.setTextColor(Color.parseColor("#FFEED8")); + unSelect.getPaint().setShader(null); + } + + void initData() { + LiveNetManager.get(getContext()) + .allGiftHall(String.valueOf(type), new HttpCallback() { + @Override + public void onSuccess(GiftWallTab2Bean data) { + if (screen != 0) { + filtrationData(data); + } + initData(data); + } + + @Override + public void onError(String error) { + + } + }); + } + + void filtrationData(GiftWallTab2Bean bean) { + Iterator iterator = bean.getIlluminateData().getGiftData().iterator(); + while (iterator.hasNext()){ + GiftWallTab2Bean.Gift next = iterator.next(); + if (screen == 1 && next.getIlluminateStatus() != 1) {//已点亮 + iterator.remove(); + } else if (screen == 2 && next.getIlluminateStatus() == 1) {//未点亮 + iterator.remove(); + } + } + } + + void initData(GiftWallTab2Bean bean) { + list1Adapter.setList(bean.getIlluminateData().getWeekStartData()); + list2Adapter.setList(bean.getIlluminateData().getGiftData()); + show_time.setText(String.format(Locale.getDefault(), "%s - %s", + bean.getGift_hall_start_date(), + bean.getGift_hall_end_date())); } @Override diff --git a/common/src/main/java/com/yunbao/common/http/PDLiveApi.java b/common/src/main/java/com/yunbao/common/http/PDLiveApi.java index cda5437c1..2418c5a91 100644 --- a/common/src/main/java/com/yunbao/common/http/PDLiveApi.java +++ b/common/src/main/java/com/yunbao/common/http/PDLiveApi.java @@ -26,8 +26,11 @@ import com.yunbao.common.bean.FansGroupGiftPackInfo; import com.yunbao.common.bean.GiftAlreadyWallModel; import com.yunbao.common.bean.GiftGuideModel; import com.yunbao.common.bean.GiftNamingInfoModel; +import com.yunbao.common.bean.GiftWallBean; import com.yunbao.common.bean.GiftWallGiftDetail; +import com.yunbao.common.bean.GiftWallInfoBean; import com.yunbao.common.bean.GiftWallModel; +import com.yunbao.common.bean.GiftWallTab2Bean; import com.yunbao.common.bean.GuardGetGuardOpenInfoModel; import com.yunbao.common.bean.GuardGetGuardUserInfoModel; import com.yunbao.common.bean.HourRank; @@ -53,7 +56,6 @@ import com.yunbao.common.bean.NobleRankHideUserListModel; import com.yunbao.common.bean.NobleTrumpetModel; import com.yunbao.common.bean.OpenAdModel; import com.yunbao.common.bean.PkRankBean; -import com.yunbao.common.bean.PrankGiftBean; import com.yunbao.common.bean.PrankGiftResultBean; import com.yunbao.common.bean.PrankHttpTurntableBean; import com.yunbao.common.bean.PrankProgressBean; @@ -1266,4 +1268,50 @@ public interface PDLiveApi { Observable> getPrankList( @Query("type")String type ); + + /** + * 个人展馆 + */ + @GET("/api/public/?service=Gift.singleUserGiftHall") + Observable> singleUserGiftHall( + @Query("user_id")String userId, + @Query("screen")int screen + ); + + /** + * 主播展馆 + */ + @GET("/api/public/?service=Gift.liveGiftHall") + Observable> liveGiftHall( + @Query("live_id")String userId, + @Query("screen")int screen + ); + + /** + * 个人展馆详情 + */ + @GET("/api/public/?service=Gift.singleUserGiftHallDetail") + Observable> singleUserGiftHallDetail( + @Query("user_id")String userId, + @Query("gift_id")String gift_id + ); + + /** + * 主播展馆详情 + */ + @GET("/api/public/?service=Gift.liveGiftHallDetail") + Observable> liveGiftHallDetail( + @Query("live_id")String live_id, + @Query("gift_id")String gift_id, + @Query("gift_hall_type")int gift_hall_type, + @Query("list_type")int list_type + ); + + /** + * 全站展馆 + */ + @GET("/api/public/?service=Gift.allGiftHall") + Observable> allGiftHall( + @Query("type")String type + ); } diff --git a/common/src/main/java/com/yunbao/common/http/live/LiveNetManager.java b/common/src/main/java/com/yunbao/common/http/live/LiveNetManager.java index 0aa79bb7d..50a6093f9 100644 --- a/common/src/main/java/com/yunbao/common/http/live/LiveNetManager.java +++ b/common/src/main/java/com/yunbao/common/http/live/LiveNetManager.java @@ -29,8 +29,11 @@ import com.yunbao.common.bean.FansGroupGiftPackInfo; import com.yunbao.common.bean.GiftAlreadyWallModel; import com.yunbao.common.bean.GiftGuideModel; import com.yunbao.common.bean.GiftNamingInfoModel; +import com.yunbao.common.bean.GiftWallBean; import com.yunbao.common.bean.GiftWallGiftDetail; +import com.yunbao.common.bean.GiftWallInfoBean; import com.yunbao.common.bean.GiftWallModel; +import com.yunbao.common.bean.GiftWallTab2Bean; import com.yunbao.common.bean.GuardGetGuardOpenInfoModel; import com.yunbao.common.bean.GuardGetGuardUserInfoModel; import com.yunbao.common.bean.HttpCallbackModel; @@ -53,7 +56,6 @@ import com.yunbao.common.bean.NobleRankHideUserListModel; import com.yunbao.common.bean.NobleTrumpetModel; import com.yunbao.common.bean.OpenAdModel; import com.yunbao.common.bean.PkRankBean; -import com.yunbao.common.bean.PrankGiftBean; import com.yunbao.common.bean.PrankGiftResultBean; import com.yunbao.common.bean.PrankHttpTurntableBean; import com.yunbao.common.bean.PrankProgressBean; @@ -99,7 +101,6 @@ import io.reactivex.schedulers.Schedulers; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; -import retrofit2.http.Query; /** @@ -3262,6 +3263,106 @@ public class LiveNetManager { } }).isDisposed(); + } + public void singleUserGiftHall(String userId,int screen, HttpCallbackcallback) { + API.get().pdLiveApi(mContext) + .singleUserGiftHall(userId,screen) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(listResponseModel -> { + if (callback != null) { + callback.onSuccess(listResponseModel.getData().getInfo()); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + if (callback != null) { + callback.onError(mContext.getString(R.string.net_error)); + } + } + }).isDisposed(); + + } + public void liveGiftHall(String userId,int screen, HttpCallbackcallback) { + API.get().pdLiveApi(mContext) + .liveGiftHall(userId,screen) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(listResponseModel -> { + if (callback != null) { + callback.onSuccess(listResponseModel.getData().getInfo()); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + if (callback != null) { + callback.onError(mContext.getString(R.string.net_error)); + } + } + }).isDisposed(); + + } + public void singleUserGiftHallDetail(String userId,String giftId, HttpCallbackcallback) { + API.get().pdLiveApi(mContext) + .singleUserGiftHallDetail(userId,giftId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(listResponseModel -> { + if (callback != null) { + callback.onSuccess(listResponseModel.getData().getInfo()); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + if (callback != null) { + callback.onError(mContext.getString(R.string.net_error)); + } + } + }).isDisposed(); + + } + public void liveGiftHallDetail(String liveId,String giftId,int gift_hall_type,int list_type, HttpCallbackcallback) { + API.get().pdLiveApi(mContext) + .liveGiftHallDetail(liveId,giftId,gift_hall_type,list_type) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(listResponseModel -> { + if (callback != null) { + callback.onSuccess(listResponseModel.getData().getInfo()); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + if (callback != null) { + callback.onError(mContext.getString(R.string.net_error)); + } + } + }).isDisposed(); + + } + public void allGiftHall(String type, HttpCallbackcallback) { + API.get().pdLiveApi(mContext) + .allGiftHall(type) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(listResponseModel -> { + if (callback != null) { + callback.onSuccess(listResponseModel.getData().getInfo()); + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + throwable.printStackTrace(); + if (callback != null) { + callback.onError(mContext.getString(R.string.net_error)); + } + } + }).isDisposed(); + } public void updateFile(File file, HttpCallback callback) { diff --git a/common/src/main/java/com/yunbao/common/utils/SVGAViewUtils.java b/common/src/main/java/com/yunbao/common/utils/SVGAViewUtils.java index 7cefb2e2f..1e72292da 100644 --- a/common/src/main/java/com/yunbao/common/utils/SVGAViewUtils.java +++ b/common/src/main/java/com/yunbao/common/utils/SVGAViewUtils.java @@ -13,13 +13,14 @@ import java.util.List; public class SVGAViewUtils { private final static List SVGA_CACHE = new ArrayList<>(); - public static void playEndClear(SVGAImageView svga, boolean isClear, SVGACallback callback) { + public static void playEndClear(SVGAImageView svga, boolean isClear,int loop, SVGACallback callback) { if (!isClear) { if (!SVGA_CACHE.contains(svga)) { SVGA_CACHE.add(svga); } } CrashSaveBean.getInstance().addPlaySvga(); + svga.setLoops(loop); svga.setCallback(new SVGACallback() { @Override public void onPause() { @@ -72,9 +73,11 @@ public class SVGAViewUtils { } SVGA_CACHE.clear(); } - + public static void playEndClear(SVGAImageView svga, boolean isClear, SVGACallback callback) { + playEndClear(svga, isClear,1, null); + } public static void playEndClear(SVGAImageView svga, boolean isClear) { - playEndClear(svga, isClear, null); + playEndClear(svga, isClear,null); } public static void playEndClear(SVGAImageView svga) { diff --git a/common/src/main/res/drawable/gift_wall_gift_info_bottom_btn_tips.xml b/common/src/main/res/drawable/gift_wall_gift_info_bottom_btn_tips.xml new file mode 100644 index 000000000..a8934c264 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_bottom_btn_tips.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_gift_info_btn.xml b/common/src/main/res/drawable/gift_wall_gift_info_btn.xml new file mode 100644 index 000000000..83a17d467 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_btn.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_gift_info_lighten.xml b/common/src/main/res/drawable/gift_wall_gift_info_lighten.xml new file mode 100644 index 000000000..775388468 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_lighten.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_gift_info_list_btn_down.xml b/common/src/main/res/drawable/gift_wall_gift_info_list_btn_down.xml new file mode 100644 index 000000000..faf44ecf9 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_list_btn_down.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_gift_info_list_btn_up.xml b/common/src/main/res/drawable/gift_wall_gift_info_list_btn_up.xml new file mode 100644 index 000000000..06cade22e --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_list_btn_up.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_gift_info_progres.xml b/common/src/main/res/drawable/gift_wall_gift_info_progres.xml new file mode 100644 index 000000000..2a8fb6f44 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_progres.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_gift_info_un_lighten.xml b/common/src/main/res/drawable/gift_wall_gift_info_un_lighten.xml new file mode 100644 index 000000000..dc4761119 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_gift_info_un_lighten.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_main_tab_list_item_progres.xml b/common/src/main/res/drawable/gift_wall_main_tab_list_item_progres.xml new file mode 100644 index 000000000..3e1da33e2 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_main_tab_list_item_progres.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_man_tab2_dsl.xml b/common/src/main/res/drawable/gift_wall_man_tab2_dsl.xml new file mode 100644 index 000000000..4caa9da97 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_man_tab2_dsl.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/drawable/gift_wall_man_tab_list_title.xml b/common/src/main/res/drawable/gift_wall_man_tab_list_title.xml new file mode 100644 index 000000000..e62c38a90 --- /dev/null +++ b/common/src/main/res/drawable/gift_wall_man_tab_list_title.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/layout/dialog_gift_wall_gift_info.xml b/common/src/main/res/layout/dialog_gift_wall_gift_info.xml new file mode 100644 index 000000000..06bb03eee --- /dev/null +++ b/common/src/main/res/layout/dialog_gift_wall_gift_info.xml @@ -0,0 +1,371 @@ + + + + + + + + + + + + +