Loading app/ui/build.gradle +2 −1 Original line number Diff line number Diff line Loading @@ -29,12 +29,13 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}" implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}" implementation "androidx.cardview:cardview:${versions.androidxCardView}" implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "com.google.android.material:material:${versions.materialComponents}" implementation "de.cketti.library.changelog:ckchangelog:1.2.1" implementation "com.github.bumptech.glide:glide:3.6.1" implementation "com.splitwise:tokenautocomplete:2.0.7" implementation "de.cketti.safecontentresolver:safe-content-resolver-v21:0.9.0" implementation "com.github.amlcurran.showcaseview:library:5.4.1" implementation "com.xwray:groupie:2.8.0" implementation "com.xwray:groupie-kotlin-android-extensions:2.8.0" implementation 'com.mikepenz:materialdrawer:7.0.0' Loading app/ui/src/main/java/com/fsck/k9/ui/compose/SimpleHighlightView.kt 0 → 100644 +230 −0 Original line number Diff line number Diff line /* * Copyright 2020 The K-9 Dog Walkers * * Based on ShowcaseView (https://github.com/amlcurran/ShowcaseView) * Copyright 2014 Alex Curran * * 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 com.fsck.k9.ui.compose import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.Activity import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Point import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import com.fsck.k9.ui.R /** * A view which allows you to highlight a view in your Activity. */ class SimpleHighlightView private constructor(context: Context, style: Int) : FrameLayout(context) { private val backgroundColor: Int private val fadeInMillis = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() private val fadeOutMillis = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() private val radius: Float = resources.getDimension(R.dimen.highlight_radius) private val basicPaint = Paint() private val eraserPaint = Paint().apply { color = 0xFFFFFF alpha = 0 xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) isAntiAlias = true } private val positionInWindow = IntArray(2) private var parent: ViewGroup? = null private var highlightX = -1 private var highlightY = -1 private var bitmapBuffer: Bitmap? = null init { val styled = getContext().obtainStyledAttributes(style, R.styleable.SimpleHighlightView) backgroundColor = styled.getColor( R.styleable.SimpleHighlightView_highlightBackgroundColor, Color.argb(128, 80, 80, 80) ) styled.recycle() } fun remove() { fadeOutHighlightAndRemoveFromParent() } override fun dispatchDraw(canvas: Canvas) { val highlightX = this.highlightX val highlightY = this.highlightY val bitmapBuffer = this.bitmapBuffer if (highlightX < 0 || highlightY < 0 || bitmapBuffer == null) { super.dispatchDraw(canvas) return } // Draw background color erase(bitmapBuffer) drawHighlightCircle(bitmapBuffer, highlightX.toFloat(), highlightY.toFloat()) drawToCanvas(canvas, bitmapBuffer) super.dispatchDraw(canvas) } private fun erase(bitmapBuffer: Bitmap) { bitmapBuffer.eraseColor(backgroundColor) } private fun drawHighlightCircle(buffer: Bitmap, x: Float, y: Float) { Canvas(buffer).apply { drawCircle(x, y, radius, eraserPaint) } } private fun drawToCanvas(canvas: Canvas, bitmapBuffer: Bitmap) { canvas.drawBitmap(bitmapBuffer, 0f, 0f, basicPaint) } private fun setHighlightPosition(x: Int, y: Int) { getLocationInWindow(positionInWindow) highlightX = x - positionInWindow[0] highlightY = y - positionInWindow[1] invalidate() } private fun setParent(parent: ViewGroup) { this.parent = parent } private fun setTarget(targetView: View) { postDelayed({ if (canUpdateBitmap()) { updateBitmap() } val point = targetView.getHighlightPoint() setHighlightPosition(point.x, point.y) }, 100) } private fun canUpdateBitmap(): Boolean { return measuredHeight > 0 && measuredWidth > 0 } private fun updateBitmap() { bitmapBuffer.let { bitmapBuffer -> if (bitmapBuffer == null || bitmapBuffer.haveBoundsChanged()) { bitmapBuffer?.recycle() this.bitmapBuffer = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888) } } } private fun Bitmap.haveBoundsChanged(): Boolean { return measuredWidth != width || measuredHeight != height } private fun View.getHighlightPoint(): Point { val location = IntArray(2) getLocationInWindow(location) val x = location[0] + width / 2 val y = location[1] + height / 2 return Point(x, y) } private fun clearBitmap() { bitmapBuffer?.let { bitmapBuffer -> if (!bitmapBuffer.isRecycled) { bitmapBuffer.recycle() this.bitmapBuffer = null } } } private fun show() { if (canUpdateBitmap()) { updateBitmap() } fadeInHighlight() } private fun fadeInHighlight() { ObjectAnimator.ofFloat(this, ALPHA, INVISIBLE, VISIBLE) .setDuration(fadeInMillis) .onAnimationStart { visibility = View.VISIBLE } .start() } private fun fadeOutHighlightAndRemoveFromParent() { ObjectAnimator.ofFloat(this, ALPHA, INVISIBLE) .setDuration(fadeOutMillis) .onAnimationEnd { visibility = View.GONE clearBitmap() parent?.removeView(this@SimpleHighlightView) } .start() } private inline fun ObjectAnimator.onAnimationStart(crossinline block: () -> Unit): ObjectAnimator { addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { block() } }) return this } private inline fun ObjectAnimator.onAnimationEnd(crossinline block: () -> Unit): ObjectAnimator { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { block() } }) return this } companion object { private const val ALPHA = "alpha" private const val INVISIBLE = 0f private const val VISIBLE = 1f @JvmStatic fun createAndInsert(activity: Activity, targetView: View, style: Int): SimpleHighlightView { val highlightView = SimpleHighlightView(activity, style) highlightView.setTarget(targetView) val parent = activity.findViewById<ViewGroup>(android.R.id.content) highlightView.setParent(parent) val parentIndex = parent.childCount parent.addView(highlightView, parentIndex) highlightView.show() return highlightView } } } app/ui/src/main/java/com/fsck/k9/view/HighlightDialogFragment.java +11 −19 Original line number Diff line number Diff line Loading @@ -10,9 +10,7 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; import com.fsck.k9.ui.R; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.ShowcaseView.Builder; import com.github.amlcurran.showcaseview.targets.ViewTarget; import com.fsck.k9.ui.compose.SimpleHighlightView; public class HighlightDialogFragment extends DialogFragment { Loading @@ -20,7 +18,7 @@ public class HighlightDialogFragment extends DialogFragment { public static final float BACKGROUND_DIM_AMOUNT = 0.25f; private ShowcaseView showcaseView; private SimpleHighlightView highlightView; protected void highlightViewInBackground() { Loading @@ -33,20 +31,14 @@ public class HighlightDialogFragment extends DialogFragment { throw new IllegalStateException("fragment must be attached to set highlight!"); } boolean alreadyShowing = showcaseView != null && showcaseView.isShowing(); boolean alreadyShowing = highlightView != null; if (alreadyShowing) { return; } int highlightedView = getArguments().getInt(ARG_HIGHLIGHT_VIEW); showcaseView = new Builder(activity) .setTarget(new ViewTarget(highlightedView, activity)) .hideOnTouchOutside() .blockAllTouches() .withMaterialShowcase() .setStyle(R.style.ShowcaseTheme) .build(); showcaseView.hideButton(); int highlightedViewId = getArguments().getInt(ARG_HIGHLIGHT_VIEW); View highlightedView = activity.findViewById(highlightedViewId); highlightView = SimpleHighlightView.createAndInsert(activity, highlightedView, R.style.MessageComposeHighlight); } @Override Loading @@ -62,7 +54,7 @@ public class HighlightDialogFragment extends DialogFragment { public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); hideShowcaseView(); hideHighlightView(); } private void setDialogBackgroundDim() { Loading @@ -89,10 +81,10 @@ public class HighlightDialogFragment extends DialogFragment { inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } private void hideShowcaseView() { if (showcaseView != null && showcaseView.isShowing()) { showcaseView.hide(); private void hideHighlightView() { if (highlightView != null) { highlightView.remove(); highlightView = null; } showcaseView = null; } } app/ui/src/main/res/values/attrs.xml +4 −0 Original line number Diff line number Diff line Loading @@ -128,4 +128,8 @@ <attr name="summaryOff" format="string" /> </declare-styleable> <declare-styleable name="SimpleHighlightView"> <attr name="highlightBackgroundColor" format="color|reference" /> </declare-styleable> </resources> app/ui/src/main/res/values/dimensions.xml +1 −0 Original line number Diff line number Diff line Loading @@ -2,4 +2,5 @@ <resources> <dimen name="button_minWidth">100sp</dimen> <dimen name="widget_padding">8dp</dimen> <dimen name="highlight_radius">48dp</dimen> </resources> Loading
app/ui/build.gradle +2 −1 Original line number Diff line number Diff line Loading @@ -29,12 +29,13 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}" implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}" implementation "androidx.cardview:cardview:${versions.androidxCardView}" implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "com.google.android.material:material:${versions.materialComponents}" implementation "de.cketti.library.changelog:ckchangelog:1.2.1" implementation "com.github.bumptech.glide:glide:3.6.1" implementation "com.splitwise:tokenautocomplete:2.0.7" implementation "de.cketti.safecontentresolver:safe-content-resolver-v21:0.9.0" implementation "com.github.amlcurran.showcaseview:library:5.4.1" implementation "com.xwray:groupie:2.8.0" implementation "com.xwray:groupie-kotlin-android-extensions:2.8.0" implementation 'com.mikepenz:materialdrawer:7.0.0' Loading
app/ui/src/main/java/com/fsck/k9/ui/compose/SimpleHighlightView.kt 0 → 100644 +230 −0 Original line number Diff line number Diff line /* * Copyright 2020 The K-9 Dog Walkers * * Based on ShowcaseView (https://github.com/amlcurran/ShowcaseView) * Copyright 2014 Alex Curran * * 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 com.fsck.k9.ui.compose import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.Activity import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Point import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import com.fsck.k9.ui.R /** * A view which allows you to highlight a view in your Activity. */ class SimpleHighlightView private constructor(context: Context, style: Int) : FrameLayout(context) { private val backgroundColor: Int private val fadeInMillis = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() private val fadeOutMillis = resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() private val radius: Float = resources.getDimension(R.dimen.highlight_radius) private val basicPaint = Paint() private val eraserPaint = Paint().apply { color = 0xFFFFFF alpha = 0 xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) isAntiAlias = true } private val positionInWindow = IntArray(2) private var parent: ViewGroup? = null private var highlightX = -1 private var highlightY = -1 private var bitmapBuffer: Bitmap? = null init { val styled = getContext().obtainStyledAttributes(style, R.styleable.SimpleHighlightView) backgroundColor = styled.getColor( R.styleable.SimpleHighlightView_highlightBackgroundColor, Color.argb(128, 80, 80, 80) ) styled.recycle() } fun remove() { fadeOutHighlightAndRemoveFromParent() } override fun dispatchDraw(canvas: Canvas) { val highlightX = this.highlightX val highlightY = this.highlightY val bitmapBuffer = this.bitmapBuffer if (highlightX < 0 || highlightY < 0 || bitmapBuffer == null) { super.dispatchDraw(canvas) return } // Draw background color erase(bitmapBuffer) drawHighlightCircle(bitmapBuffer, highlightX.toFloat(), highlightY.toFloat()) drawToCanvas(canvas, bitmapBuffer) super.dispatchDraw(canvas) } private fun erase(bitmapBuffer: Bitmap) { bitmapBuffer.eraseColor(backgroundColor) } private fun drawHighlightCircle(buffer: Bitmap, x: Float, y: Float) { Canvas(buffer).apply { drawCircle(x, y, radius, eraserPaint) } } private fun drawToCanvas(canvas: Canvas, bitmapBuffer: Bitmap) { canvas.drawBitmap(bitmapBuffer, 0f, 0f, basicPaint) } private fun setHighlightPosition(x: Int, y: Int) { getLocationInWindow(positionInWindow) highlightX = x - positionInWindow[0] highlightY = y - positionInWindow[1] invalidate() } private fun setParent(parent: ViewGroup) { this.parent = parent } private fun setTarget(targetView: View) { postDelayed({ if (canUpdateBitmap()) { updateBitmap() } val point = targetView.getHighlightPoint() setHighlightPosition(point.x, point.y) }, 100) } private fun canUpdateBitmap(): Boolean { return measuredHeight > 0 && measuredWidth > 0 } private fun updateBitmap() { bitmapBuffer.let { bitmapBuffer -> if (bitmapBuffer == null || bitmapBuffer.haveBoundsChanged()) { bitmapBuffer?.recycle() this.bitmapBuffer = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888) } } } private fun Bitmap.haveBoundsChanged(): Boolean { return measuredWidth != width || measuredHeight != height } private fun View.getHighlightPoint(): Point { val location = IntArray(2) getLocationInWindow(location) val x = location[0] + width / 2 val y = location[1] + height / 2 return Point(x, y) } private fun clearBitmap() { bitmapBuffer?.let { bitmapBuffer -> if (!bitmapBuffer.isRecycled) { bitmapBuffer.recycle() this.bitmapBuffer = null } } } private fun show() { if (canUpdateBitmap()) { updateBitmap() } fadeInHighlight() } private fun fadeInHighlight() { ObjectAnimator.ofFloat(this, ALPHA, INVISIBLE, VISIBLE) .setDuration(fadeInMillis) .onAnimationStart { visibility = View.VISIBLE } .start() } private fun fadeOutHighlightAndRemoveFromParent() { ObjectAnimator.ofFloat(this, ALPHA, INVISIBLE) .setDuration(fadeOutMillis) .onAnimationEnd { visibility = View.GONE clearBitmap() parent?.removeView(this@SimpleHighlightView) } .start() } private inline fun ObjectAnimator.onAnimationStart(crossinline block: () -> Unit): ObjectAnimator { addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { block() } }) return this } private inline fun ObjectAnimator.onAnimationEnd(crossinline block: () -> Unit): ObjectAnimator { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { block() } }) return this } companion object { private const val ALPHA = "alpha" private const val INVISIBLE = 0f private const val VISIBLE = 1f @JvmStatic fun createAndInsert(activity: Activity, targetView: View, style: Int): SimpleHighlightView { val highlightView = SimpleHighlightView(activity, style) highlightView.setTarget(targetView) val parent = activity.findViewById<ViewGroup>(android.R.id.content) highlightView.setParent(parent) val parentIndex = parent.childCount parent.addView(highlightView, parentIndex) highlightView.show() return highlightView } } }
app/ui/src/main/java/com/fsck/k9/view/HighlightDialogFragment.java +11 −19 Original line number Diff line number Diff line Loading @@ -10,9 +10,7 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; import com.fsck.k9.ui.R; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.ShowcaseView.Builder; import com.github.amlcurran.showcaseview.targets.ViewTarget; import com.fsck.k9.ui.compose.SimpleHighlightView; public class HighlightDialogFragment extends DialogFragment { Loading @@ -20,7 +18,7 @@ public class HighlightDialogFragment extends DialogFragment { public static final float BACKGROUND_DIM_AMOUNT = 0.25f; private ShowcaseView showcaseView; private SimpleHighlightView highlightView; protected void highlightViewInBackground() { Loading @@ -33,20 +31,14 @@ public class HighlightDialogFragment extends DialogFragment { throw new IllegalStateException("fragment must be attached to set highlight!"); } boolean alreadyShowing = showcaseView != null && showcaseView.isShowing(); boolean alreadyShowing = highlightView != null; if (alreadyShowing) { return; } int highlightedView = getArguments().getInt(ARG_HIGHLIGHT_VIEW); showcaseView = new Builder(activity) .setTarget(new ViewTarget(highlightedView, activity)) .hideOnTouchOutside() .blockAllTouches() .withMaterialShowcase() .setStyle(R.style.ShowcaseTheme) .build(); showcaseView.hideButton(); int highlightedViewId = getArguments().getInt(ARG_HIGHLIGHT_VIEW); View highlightedView = activity.findViewById(highlightedViewId); highlightView = SimpleHighlightView.createAndInsert(activity, highlightedView, R.style.MessageComposeHighlight); } @Override Loading @@ -62,7 +54,7 @@ public class HighlightDialogFragment extends DialogFragment { public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); hideShowcaseView(); hideHighlightView(); } private void setDialogBackgroundDim() { Loading @@ -89,10 +81,10 @@ public class HighlightDialogFragment extends DialogFragment { inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } private void hideShowcaseView() { if (showcaseView != null && showcaseView.isShowing()) { showcaseView.hide(); private void hideHighlightView() { if (highlightView != null) { highlightView.remove(); highlightView = null; } showcaseView = null; } }
app/ui/src/main/res/values/attrs.xml +4 −0 Original line number Diff line number Diff line Loading @@ -128,4 +128,8 @@ <attr name="summaryOff" format="string" /> </declare-styleable> <declare-styleable name="SimpleHighlightView"> <attr name="highlightBackgroundColor" format="color|reference" /> </declare-styleable> </resources>
app/ui/src/main/res/values/dimensions.xml +1 −0 Original line number Diff line number Diff line Loading @@ -2,4 +2,5 @@ <resources> <dimen name="button_minWidth">100sp</dimen> <dimen name="widget_padding">8dp</dimen> <dimen name="highlight_radius">48dp</dimen> </resources>