diff --git a/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt b/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt similarity index 93% rename from app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt rename to app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt index 441efc6e0..53141f492 100644 --- a/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.widget +package io.legado.app.ui.widget.anima import android.animation.Animator import android.animation.AnimatorSet @@ -61,7 +61,8 @@ class RotateLoading : View { loadingColor = Color.WHITE thisWidth = dpToPx(context, DEFAULT_WIDTH.toFloat()) shadowPosition = dpToPx(getContext(), DEFAULT_SHADOW_POSITION.toFloat()) - speedOfDegree = DEFAULT_SPEED_OF_DEGREE + speedOfDegree = + DEFAULT_SPEED_OF_DEGREE if (null != attrs) { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RotateLoading) @@ -70,8 +71,14 @@ class RotateLoading : View { R.styleable.RotateLoading_loading_width, dpToPx(context, DEFAULT_WIDTH.toFloat()) ) - shadowPosition = typedArray.getInt(R.styleable.RotateLoading_shadow_position, DEFAULT_SHADOW_POSITION) - speedOfDegree = typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE) + shadowPosition = typedArray.getInt( + R.styleable.RotateLoading_shadow_position, + DEFAULT_SHADOW_POSITION + ) + speedOfDegree = typedArray.getInt( + R.styleable.RotateLoading_loading_speed, + DEFAULT_SPEED_OF_DEGREE + ) typedArray.recycle() } speedOfArc = (speedOfDegree / 4).toFloat() diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt new file mode 100644 index 000000000..28bb1602a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.ValueAnimator +import android.graphics.* +import android.view.View +import android.view.animation.AccelerateInterpolator +import java.util.* + +class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rect) : + ValueAnimator() { + private val mPaint: Paint + private val mParticles: Array + private val mBound: Rect + + init { + mPaint = Paint() + mBound = Rect(bound) + val partLen = 15 + mParticles = arrayOfNulls(partLen * partLen) + val random = Random(System.currentTimeMillis()) + val w = bitmap.width / (partLen + 2) + val h = bitmap.height / (partLen + 2) + for (i in 0 until partLen) { + for (j in 0 until partLen) { + mParticles[i * partLen + j] = + generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random) + } + } + setFloatValues(0f, END_VALUE) + interpolator = DEFAULT_INTERPOLATOR + duration = DEFAULT_DURATION + } + + private fun generateParticle(color: Int, random: Random): Particle { + val particle = Particle() + particle.color = color + particle.radius = V + if (random.nextFloat() < 0.2f) { + particle.baseRadius = V + (X - V) * random.nextFloat() + } else { + particle.baseRadius = W + (V - W) * random.nextFloat() + } + val nextFloat = random.nextFloat() + particle.top = mBound.height() * (0.18f * random.nextFloat() + 0.2f) + particle.top = + if (nextFloat < 0.2f) particle.top else particle.top + particle.top * 0.2f * random.nextFloat() + particle.bottom = mBound.height() * (random.nextFloat() - 0.5f) * 1.8f + var f = + if (nextFloat < 0.2f) particle.bottom else if (nextFloat < 0.8f) particle.bottom * 0.6f else particle.bottom * 0.3f + particle.bottom = f + particle.mag = 4.0f * particle.top / particle.bottom + particle.neg = -particle.mag / particle.bottom + f = mBound.centerX() + Y * (random.nextFloat() - 0.5f) + particle.baseCx = f + particle.cx = f + f = mBound.centerY() + Y * (random.nextFloat() - 0.5f) + particle.baseCy = f + particle.cy = f + particle.life = END_VALUE / 10 * random.nextFloat() + particle.overflow = 0.4f * random.nextFloat() + particle.alpha = 1f + return particle + } + + fun draw(canvas: Canvas): Boolean { + if (!isStarted) { + return false + } + for (particle in mParticles) { + particle?.let { + particle.advance(animatedValue as Float) + if (particle.alpha > 0f) { + mPaint.color = particle.color + mPaint.alpha = (Color.alpha(particle.color) * particle.alpha).toInt() + canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint) + } + } + } + mContainer.invalidate() + return true + } + + override fun start() { + super.start() + mContainer.invalidate(mBound) + } + + private inner class Particle { + internal var alpha: Float = 0.toFloat() + internal var color: Int = 0 + internal var cx: Float = 0.toFloat() + internal var cy: Float = 0.toFloat() + internal var radius: Float = 0.toFloat() + internal var baseCx: Float = 0.toFloat() + internal var baseCy: Float = 0.toFloat() + internal var baseRadius: Float = 0.toFloat() + internal var top: Float = 0.toFloat() + internal var bottom: Float = 0.toFloat() + internal var mag: Float = 0.toFloat() + internal var neg: Float = 0.toFloat() + internal var life: Float = 0.toFloat() + internal var overflow: Float = 0.toFloat() + + + fun advance(factor: Float) { + var f = 0f + var normalization = factor / END_VALUE + if (normalization < life || normalization > 1f - overflow) { + alpha = 0f + return + } + normalization = (normalization - life) / (1f - life - overflow) + val f2 = normalization * END_VALUE + if (normalization >= 0.7f) { + f = (normalization - 0.7f) / 0.3f + } + alpha = 1f - f + f = bottom * f2 + cx = baseCx + f + cy = (baseCy - this.neg * Math.pow(f.toDouble(), 2.0)).toFloat() - f * mag + radius = V + (baseRadius - V) * f2 + } + } + + companion object { + + internal var DEFAULT_DURATION: Long = 0x400 + private val DEFAULT_INTERPOLATOR = AccelerateInterpolator(0.6f) + private val END_VALUE = 1.4f + private val X = Utils.dp2Px(5).toFloat() + private val Y = Utils.dp2Px(20).toFloat() + private val V = Utils.dp2Px(2).toFloat() + private val W = Utils.dp2Px(1).toFloat() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt new file mode 100644 index 000000000..1332a3969 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.app.Activity +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.media.MediaPlayer +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.Window +import java.util.* + + +class ExplosionField : View { + + private var customDuration = ExplosionAnimator.DEFAULT_DURATION + private var idPlayAnimationEffect = 0 + private var mZAnimatorListener: OnAnimatorListener? = null + private var mOnClickListener: View.OnClickListener? = null + + private val mExplosions = ArrayList() + private val mExpandInset = IntArray(2) + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init() + } + + private fun init() { + + Arrays.fill(mExpandInset, Utils.dp2Px(32)) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + for (explosion in mExplosions) { + explosion.draw(canvas) + } + } + + fun playSoundAnimationEffect(id: Int) { + this.idPlayAnimationEffect = id + } + + fun setCustomDuration(customDuration: Long) { + this.customDuration = customDuration + } + + fun addActionEvent(ievents: OnAnimatorListener) { + this.mZAnimatorListener = ievents + } + + + fun expandExplosionBound(dx: Int, dy: Int) { + mExpandInset[0] = dx + mExpandInset[1] = dy + } + + @JvmOverloads + fun explode(bitmap: Bitmap?, bound: Rect, startDelay: Long, view: View? = null) { + val currentDuration = customDuration + val explosion = ExplosionAnimator(this, bitmap!!, bound) + explosion.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mExplosions.remove(animation) + if (view != null) { + view.scaleX = 1f + view.scaleY = 1f + view.alpha = 1f + view.setOnClickListener(mOnClickListener)//set event + + } + } + }) + explosion.startDelay = startDelay + explosion.duration = currentDuration + mExplosions.add(explosion) + explosion.start() + } + + @JvmOverloads + fun explode(view: View, restartState: Boolean? = false) { + + val r = Rect() + view.getGlobalVisibleRect(r) + val location = IntArray(2) + getLocationOnScreen(location) + // getLocationInWindow(location); + // view.getLocationInWindow(location); + r.offset(-location[0], -location[1]) + r.inset(-mExpandInset[0], -mExpandInset[1]) + val startDelay = 100 + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150) + animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { + + internal var random = Random() + + override fun onAnimationUpdate(animation: ValueAnimator) { + view.translationX = (random.nextFloat() - 0.5f) * view.width.toFloat() * 0.05f + view.translationY = (random.nextFloat() - 0.5f) * view.height.toFloat() * 0.05f + } + }) + + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) { + if (idPlayAnimationEffect != 0) + MediaPlayer.create(context, idPlayAnimationEffect).start() + } + + override fun onAnimationEnd(animator: Animator) { + if (mZAnimatorListener != null) { + mZAnimatorListener!!.onAnimationEnd(animator, this@ExplosionField) + } + } + + override fun onAnimationCancel(animator: Animator) { + Log.i("PRUEBA", "CANCEL") + } + + override fun onAnimationRepeat(animator: Animator) { + Log.i("PRUEBA", "REPEAT") + } + }) + + animator.start() + view.animate().setDuration(150).setStartDelay(startDelay.toLong()).scaleX(0f).scaleY(0f) + .alpha(0f).start() + if (restartState!!) + explode(Utils.createBitmapFromView(view), r, startDelay.toLong(), view) + else + explode(Utils.createBitmapFromView(view), r, startDelay.toLong()) + + } + + fun clear() { + mExplosions.clear() + invalidate() + } + + override fun setOnClickListener(mOnClickListener: View.OnClickListener?) { + this.mOnClickListener = mOnClickListener + } + + companion object { + + fun attach2Window(activity: Activity): ExplosionField { + val rootView = activity.findViewById(Window.ID_ANDROID_CONTENT) as ViewGroup + val explosionField = ExplosionField(activity) + rootView.addView( + explosionField, ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + return explosionField + } + } + + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt new file mode 100644 index 000000000..13a04c670 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt @@ -0,0 +1,8 @@ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.Animator +import android.view.View + +interface OnAnimatorListener { + fun onAnimationEnd(animator: Animator, view: View) +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt new file mode 100644 index 000000000..2f99191d2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.view.View +import android.widget.ImageView + +object Utils { + + private val DENSITY = Resources.getSystem().displayMetrics.density + private val sCanvas = Canvas() + + fun dp2Px(dp: Int): Int { + return Math.round(dp * DENSITY) + } + + fun createBitmapFromView(view: View): Bitmap? { + if (view is ImageView) { + val drawable = view.drawable + if (drawable != null && drawable is BitmapDrawable) { + return drawable.bitmap + } + } + view.clearFocus() + val bitmap = createBitmapSafely( + view.width, + view.height, Bitmap.Config.ARGB_8888, 1 + ) + if (bitmap != null) { + synchronized(sCanvas) { + val canvas = sCanvas + canvas.setBitmap(bitmap) + view.draw(canvas) + canvas.setBitmap(null) + } + } + return bitmap + } + + fun createBitmapSafely( + width: Int, + height: Int, + config: Bitmap.Config, + retryCount: Int + ): Bitmap? { + try { + return Bitmap.createBitmap(width, height, config) + } catch (e: OutOfMemoryError) { + e.printStackTrace() + if (retryCount > 0) { + System.gc() + return createBitmapSafely(width, height, config, retryCount - 1) + } + return null + } + + } +}