diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 3709341..3576f96 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -72,7 +72,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 17 + java-version: 11 - uses: gradle/gradle-build-action@v2 @@ -94,13 +94,14 @@ jobs: target: google_apis arch: x86 disable-spellchecker: true + working-directory: sample script: | adb shell settings put global hidden_api_policy_p_apps 1 adb shell settings put global hidden_api_policy_pre_p_apps 1 adb shell settings put global hidden_api_policy 1 python --version pip show Pillow - ./gradlew connectedCheck + ./gradlew executeScreenshotTests - name: Upload tests result uses: actions/upload-artifact@v3 @@ -108,6 +109,13 @@ jobs: name: android-tests path: "${{ github.workspace }}/**/build/*AndroidTest/" + - name: Upload screeenshots + uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: screenshots-report + path: "${{ github.workspace }}/**/build/**/shot/" + build-sample-app: runs-on: ubuntu-latest steps: diff --git a/LICENSE b/LICENSE index e1ece4d..5e51587 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The code (most of it) is available under [BSD License](LICENSE) -```markdown +BSD License + For Shimmer-android software Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. @@ -29,10 +30,9 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -``` -the code, added in 2022 and onwards, is available under MIT License: -```markdown +the new code, added in 2022 and onwards, is available under MIT License: + MIT License Copyright (c) Developers from https://github.com/usefulness organization @@ -54,4 +54,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e76cb53..25f85df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] gradle-starter = "0.53.0" gradle-kotlinter = "3.12.0" +gradle-screenshotTesting = "0.16.10" google-agp = "7.3.1" maven-kotlin = "1.7.21" maven-junit = "5.9.1" @@ -19,10 +20,12 @@ agp-gradle = { module = "com.android.tools.build:gradle", version.ref = "google- kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "maven-kotlin" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "maven-junit" } +junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "maven-junit" } + assertj-core = { module = "org.assertj:assertj-core", version.ref = "maven-assertj" } androidx-core = { module = "androidx.core:core", version.ref = "google-androidx-core" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "google-androidx-annotation" } -androidtest-core = { module = "androidx.test:core", version.ref = "google-androidtest" } +androidtest-core = { module = "androidx.test:core-ktx", version.ref = "google-androidtest" } androidtest-runner = { module = "androidx.test:runner", version.ref = "google-androidtestRunner" } androidtest-rules = { module = "androidx.test:rules", version.ref = "google-androidtest" } androidtest-junitext = { module = "androidx.test.ext:junit-ktx", version.ref = "google-androidtestext" } @@ -38,3 +41,4 @@ starter-versioning = { id = "com.starter.versioning", version.ref = "gradle-star starter-library-kotlin = { id = "com.starter.library.kotlin", version.ref = "gradle-starter" } starter-library-android = { id = "com.starter.library.android", version.ref = "gradle-starter" } starter-application-android = { id = "com.starter.application.android", version.ref = "gradle-starter" } +starter-screenshottesting = { id = "io.github.usefulness.screenshot-testing-plugin", version.ref = "gradle-screenshotTesting" } diff --git a/sample/build.gradle b/sample/build.gradle index ff3bede..c84e954 100755 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,19 +1,34 @@ +import com.karumi.shot.tasks.ExecuteScreenshotTests + plugins { alias(libs.plugins.starter.application.android) + id("shot") } android { namespace = "io.github.usefulness.shimmer.sample" defaultConfig { + minSdk 28 applicationId "io.github.usefulness.shimmer.sample" + testInstrumentationRunner "com.karumi.shot.ShotTestRunner" } buildFeatures { viewBinding = true } } + dependencies { implementation("io.github.usefulness:shimmer-android-core") implementation(libs.appcompat.core) implementation(libs.constraintlayout.core) + + testImplementation(libs.junit.jupiter) + + androidTestImplementation(libs.appcompat.core) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.androidtest.core) + androidTestImplementation(libs.androidtest.runner) + androidTestImplementation(libs.androidtest.rules) + androidTestImplementation(libs.androidtest.junitext) } diff --git a/sample/screenshots/debug/io.github.usefulness.shimmer.android.ShimmerTest_launchView-wide.png b/sample/screenshots/debug/io.github.usefulness.shimmer.android.ShimmerTest_launchView-wide.png new file mode 100644 index 0000000..aaca851 Binary files /dev/null and b/sample/screenshots/debug/io.github.usefulness.shimmer.android.ShimmerTest_launchView-wide.png differ diff --git a/sample/screenshots/debug/io.github.usefulness.shimmer.android.ShimmerTest_launchView.png b/sample/screenshots/debug/io.github.usefulness.shimmer.android.ShimmerTest_launchView.png new file mode 100644 index 0000000..93b85b8 Binary files /dev/null and b/sample/screenshots/debug/io.github.usefulness.shimmer.android.ShimmerTest_launchView.png differ diff --git a/sample/settings.gradle b/sample/settings.gradle index b8ff72f..67cf725 100644 --- a/sample/settings.gradle +++ b/sample/settings.gradle @@ -1,4 +1,11 @@ pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == 'shot') { + useModule("com.karumi:shot:5.14.1") + } + } + } repositories { gradlePluginPortal() google() diff --git a/sample/src/androidTest/kotlin/io/github/usefulness/shimmer/android/ShimmerTest.kt b/sample/src/androidTest/kotlin/io/github/usefulness/shimmer/android/ShimmerTest.kt new file mode 100644 index 0000000..782c6c3 --- /dev/null +++ b/sample/src/androidTest/kotlin/io/github/usefulness/shimmer/android/ShimmerTest.kt @@ -0,0 +1,30 @@ +package io.github.usefulness.shimmer.android + +import android.view.LayoutInflater +import androidx.test.ext.junit.rules.activityScenarioRule +import com.karumi.shot.ActivityScenarioUtils.waitForActivity +import com.karumi.shot.ScreenshotTest +import io.github.usefulness.shimmer.sample.MainActivity +import io.github.usefulness.shimmer.sample.R +import org.junit.Rule +import org.junit.Test + +class ShimmerTest { + + val test = object : ScreenshotTest {} + + @get:Rule + val rule = activityScenarioRule() + + @Test + fun launchView() { + val activity = rule.scenario.waitForActivity() + val inflater = LayoutInflater.from(activity) + val view = inflater.inflate(R.layout.main, null, false) + val shimmer = view.findViewById(R.id.shimmer_view_container) + shimmer.setStaticAnimationProgress(0.5f) + + test.compareScreenshot(view, 2000, 1200) + test.compareScreenshot(view, 2000, 1600, name = "launchView-wide") + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 8268baa..5ba54e1 100755 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,10 +1,17 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + > + + + + + + diff --git a/sample/src/main/res/drawable/ic_wave.xml b/sample/src/main/res/drawable/ic_wave.xml new file mode 100644 index 0000000..6f51016 --- /dev/null +++ b/sample/src/main/res/drawable/ic_wave.xml @@ -0,0 +1,10 @@ + + + diff --git a/sample/src/main/res/drawable/icon.png b/sample/src/main/res/drawable/icon.png deleted file mode 100644 index 08a87a4..0000000 Binary files a/sample/src/main/res/drawable/icon.png and /dev/null differ diff --git a/sample/src/main/res/layout/main.xml b/sample/src/main/res/layout/main.xml index 7ae9aae..f2bbc27 100755 --- a/sample/src/main/res/layout/main.xml +++ b/sample/src/main/res/layout/main.xml @@ -8,7 +8,7 @@ android:padding="20dp" > - - + + + + + diff --git a/sample/src/main/res/values/ic_launcher_background.xml b/sample/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..b37cb1c --- /dev/null +++ b/sample/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #8D1F1F + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 467399e..e28a283 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - Shimmer - Facebook’s mission is to give people the power to build community and bring the world closer together. - Presets + Shimmer + Lorem ipsum dolor sit amet. Est tenetur quaerat sed consequatur cumque in accusantium nemo nam sint voluptates non modi sint nam possimus laboriosam. + Presets diff --git a/shimmer-android-core/build.gradle b/shimmer-android-core/build.gradle index 69261d1..fde82dc 100755 --- a/shimmer-android-core/build.gradle +++ b/shimmer-android-core/build.gradle @@ -35,4 +35,6 @@ tasks.withType(Test).configureEach { dependencies { implementation(libs.androidx.annotation) + + testImplementation(libs.junit.jupiter) } diff --git a/shimmer-android-core/src/main/java/com/facebook/shimmer/ShimmerFrameLayout.java b/shimmer-android-core/src/main/java/com/facebook/shimmer/ShimmerFrameLayout.java deleted file mode 100644 index b73ed2c..0000000 --- a/shimmer-android-core/src/main/java/com/facebook/shimmer/ShimmerFrameLayout.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.shimmer; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.github.usefulness.shimmer.android.R; - -/** - * Shimmer is an Android library that provides an easy way to add a shimmer effect to any {@link - * android.view.View}. It is useful as an unobtrusive loading indicator, and was originally - * developed for Facebook Home. - * - *

Find more examples and usage instructions over at: facebook.github.io/shimmer-android - */ -public class ShimmerFrameLayout extends FrameLayout { - private final Paint mContentPaint = new Paint(); - private final ShimmerDrawable mShimmerDrawable = new ShimmerDrawable(); - - private boolean mShowShimmer = true; - private boolean mStoppedShimmerBecauseVisibility = false; - - public ShimmerFrameLayout(Context context) { - super(context); - init(context, null); - } - - public ShimmerFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs); - } - - public ShimmerFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public ShimmerFrameLayout( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(context, attrs); - } - - private void init(Context context, @Nullable AttributeSet attrs) { - setWillNotDraw(false); - mShimmerDrawable.setCallback(this); - - if (attrs == null) { - setShimmer(new Shimmer.AlphaHighlightBuilder().build()); - return; - } - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShimmerFrameLayout, 0, 0); - try { - Shimmer.Builder shimmerBuilder = - a.hasValue(R.styleable.ShimmerFrameLayout_shimmer_colored) - && a.getBoolean(R.styleable.ShimmerFrameLayout_shimmer_colored, false) - ? new Shimmer.ColorHighlightBuilder() - : new Shimmer.AlphaHighlightBuilder(); - setShimmer(shimmerBuilder.consumeAttributes(a).build()); - } finally { - a.recycle(); - } - } - - public ShimmerFrameLayout setShimmer(@Nullable Shimmer shimmer) { - mShimmerDrawable.setShimmer(shimmer); - if (shimmer != null && shimmer.clipToChildren) { - setLayerType(LAYER_TYPE_HARDWARE, mContentPaint); - } else { - setLayerType(LAYER_TYPE_NONE, null); - } - - return this; - } - - public @Nullable Shimmer getShimmer() { - return mShimmerDrawable.getShimmer(); - } - - /** Starts the shimmer animation. */ - public void startShimmer() { - if (isAttachedToWindow()) { - mShimmerDrawable.startShimmer(); - } - } - - /** Stops the shimmer animation. */ - public void stopShimmer() { - mStoppedShimmerBecauseVisibility = false; - mShimmerDrawable.stopShimmer(); - } - - /** Return whether the shimmer animation has been started. */ - public boolean isShimmerStarted() { - return mShimmerDrawable.isShimmerStarted(); - } - - /** - * Sets the ShimmerDrawable to be visible. - * - * @param startShimmer Whether to start the shimmer again. - */ - public void showShimmer(boolean startShimmer) { - mShowShimmer = true; - if (startShimmer) { - startShimmer(); - } - invalidate(); - } - - /** Sets the ShimmerDrawable to be invisible, stopping it in the process. */ - public void hideShimmer() { - stopShimmer(); - mShowShimmer = false; - invalidate(); - } - - /** Return whether the shimmer drawable is visible. */ - public boolean isShimmerVisible() { - return mShowShimmer; - } - - public boolean isShimmerRunning() { - return mShimmerDrawable.isShimmerRunning(); - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - final int width = getWidth(); - final int height = getHeight(); - mShimmerDrawable.setBounds(0, 0, width, height); - } - - @Override - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - // View's constructor directly invokes this method, in which case no fields on - // this class have been fully initialized yet. - if (mShimmerDrawable == null) { - return; - } - if (visibility != View.VISIBLE) { - // GONE or INVISIBLE - if (isShimmerStarted()) { - stopShimmer(); - mStoppedShimmerBecauseVisibility = true; - } - } else if (mStoppedShimmerBecauseVisibility) { - mShimmerDrawable.maybeStartShimmer(); - mStoppedShimmerBecauseVisibility = false; - } - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - mShimmerDrawable.maybeStartShimmer(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - stopShimmer(); - } - - @Override - public void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - if (mShowShimmer) { - mShimmerDrawable.draw(canvas); - } - } - - @Override - protected boolean verifyDrawable(@NonNull Drawable who) { - return super.verifyDrawable(who) || who == mShimmerDrawable; - } - - public void setStaticAnimationProgress(float value) { - mShimmerDrawable.setStaticAnimationProgress(value); - } - - public void clearStaticAnimationProgress() { - mShimmerDrawable.clearStaticAnimationProgress(); - } -} diff --git a/shimmer-android-core/src/main/java/com/facebook/shimmer/Shimmer.java b/shimmer-android-core/src/main/java/io/github/usefulness/shimmer/android/Shimmer.java similarity index 99% rename from shimmer-android-core/src/main/java/com/facebook/shimmer/Shimmer.java rename to shimmer-android-core/src/main/java/io/github/usefulness/shimmer/android/Shimmer.java index 8c8c280..fdb6913 100644 --- a/shimmer-android-core/src/main/java/com/facebook/shimmer/Shimmer.java +++ b/shimmer-android-core/src/main/java/io/github/usefulness/shimmer/android/Shimmer.java @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.shimmer; +package io.github.usefulness.shimmer.android; import android.animation.ValueAnimator; import android.content.Context; @@ -18,9 +18,9 @@ import androidx.annotation.FloatRange; import androidx.annotation.IntDef; import androidx.annotation.Px; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import io.github.usefulness.shimmer.android.R; /** * A Shimmer is an object detailing all of the configuration options available for {@link diff --git a/shimmer-android-core/src/main/java/com/facebook/shimmer/ShimmerDrawable.java b/shimmer-android-core/src/main/java/io/github/usefulness/shimmer/android/ShimmerDrawable.java similarity index 99% rename from shimmer-android-core/src/main/java/com/facebook/shimmer/ShimmerDrawable.java rename to shimmer-android-core/src/main/java/io/github/usefulness/shimmer/android/ShimmerDrawable.java index d9940b7..b7d83b9 100644 --- a/shimmer-android-core/src/main/java/com/facebook/shimmer/ShimmerDrawable.java +++ b/shimmer-android-core/src/main/java/io/github/usefulness/shimmer/android/ShimmerDrawable.java @@ -7,7 +7,7 @@ */ -package com.facebook.shimmer; +package io.github.usefulness.shimmer.android; import android.animation.ValueAnimator; import android.graphics.Canvas; diff --git a/shimmer-android-core/src/main/kotlin/io/github/usefulness/shimmer/android/ShimmerFrameLayout.kt b/shimmer-android-core/src/main/kotlin/io/github/usefulness/shimmer/android/ShimmerFrameLayout.kt new file mode 100644 index 0000000..22bca49 --- /dev/null +++ b/shimmer-android-core/src/main/kotlin/io/github/usefulness/shimmer/android/ShimmerFrameLayout.kt @@ -0,0 +1,162 @@ +package io.github.usefulness.shimmer.android + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import io.github.usefulness.shimmer.android.Shimmer.AlphaHighlightBuilder +import io.github.usefulness.shimmer.android.Shimmer.ColorHighlightBuilder + +class ShimmerFrameLayout : FrameLayout { + + private val contentPaint = Paint() + private val shimmerDrawable = ShimmerDrawable() + + /** Return whether the shimmer drawable is visible. */ + var isShimmerVisible = true + private set + private var shimmerStoppedBecauseVisibility = false + + constructor(context: Context) : super(context) { + init(context, null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init(context, attrs) + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) { + init(context, attrs) + } + + private fun init(context: Context, attrs: AttributeSet?) { + setWillNotDraw(false) + shimmerDrawable.callback = this + if (attrs == null) { + shimmer = AlphaHighlightBuilder().build() + return + } + val attributes = context.obtainStyledAttributes(attrs, R.styleable.ShimmerFrameLayout, 0, 0) + + val shimmerBuilder = if (attributes.getBoolean(R.styleable.ShimmerFrameLayout_shimmer_colored, false)) { + ColorHighlightBuilder() + } else { + AlphaHighlightBuilder() + } + shimmer = shimmerBuilder.consumeAttributes(attributes).build() + + attributes.recycle() + } + + var shimmer + get() = shimmerDrawable.shimmer + set(value) { + shimmerDrawable.shimmer = value + if (shimmer?.clipToChildren == true) { + setLayerType(LAYER_TYPE_HARDWARE, contentPaint) + } else { + setLayerType(LAYER_TYPE_NONE, null) + } + } + + /** Starts the shimmer animation. */ + fun startShimmer() { + if (isAttachedToWindow) { + shimmerDrawable.startShimmer() + } + } + + /** Stops the shimmer animation. */ + fun stopShimmer() { + shimmerStoppedBecauseVisibility = false + shimmerDrawable.stopShimmer() + } + + /** Return whether the shimmer animation has been started. */ + val isShimmerStarted: Boolean + get() = shimmerDrawable.isShimmerStarted + + /** + * Sets the ShimmerDrawable to be visible. + * + * @param startShimmer Whether to start the shimmer again. + */ + fun showShimmer(startShimmer: Boolean) { + isShimmerVisible = true + if (startShimmer) { + startShimmer() + } + invalidate() + } + + /** Sets the ShimmerDrawable to be invisible, stopping it in the process. */ + fun hideShimmer() { + stopShimmer() + isShimmerVisible = false + invalidate() + } + + val isShimmerRunning: Boolean + get() = shimmerDrawable.isShimmerRunning + + public override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + val width = width + val height = height + shimmerDrawable.setBounds(0, 0, width, height) + } + + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + if (visibility != VISIBLE) { + if (isShimmerStarted) { + stopShimmer() + shimmerStoppedBecauseVisibility = true + } + } else if (shimmerStoppedBecauseVisibility) { + shimmerDrawable.maybeStartShimmer() + shimmerStoppedBecauseVisibility = false + } + } + + public override fun onAttachedToWindow() { + super.onAttachedToWindow() + shimmerDrawable.maybeStartShimmer() + } + + public override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopShimmer() + } + + public override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + if (isShimmerVisible) { + shimmerDrawable.draw(canvas) + } + } + + override fun verifyDrawable(who: Drawable): Boolean { + return super.verifyDrawable(who) || who === shimmerDrawable + } + + fun setStaticAnimationProgress(value: Float) { + shimmerDrawable.setStaticAnimationProgress(value) + } + + fun clearStaticAnimationProgress() { + shimmerDrawable.clearStaticAnimationProgress() + } +}