diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..347e252 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2d6ac0 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# CircleViewImagektx + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..55a677d --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "com.banrossyn.circleviewimagektx" + compileSdk = 34 + + defaultConfig { + applicationId = "com.banrossyn.circleviewimagektx" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/banrossyn/circleviewimagektx/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/banrossyn/circleviewimagektx/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3320bd1 --- /dev/null +++ b/app/src/androidTest/java/com/banrossyn/circleviewimagektx/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.banrossyn.circleviewimagektx + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.banrossyn.circleviewimagektx", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5f4b848 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/banrossyn/circleviewimagektx/CircleImageView.kt b/app/src/main/java/com/banrossyn/circleviewimagektx/CircleImageView.kt new file mode 100644 index 0000000..5b3a222 --- /dev/null +++ b/app/src/main/java/com/banrossyn/circleviewimagektx/CircleImageView.kt @@ -0,0 +1,585 @@ +package com.banrossyn.circleviewimagektx + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.TypedArray +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Matrix +import android.graphics.Outline +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatImageView + + +import kotlin.math.min +import kotlin.math.pow +/** + * A custom `ImageView` that displays images in a circular shape, with additional features like + * borders, circle background color, and support for padding adjustments. This view extends + * `AppCompatImageView` to provide backward compatibility for older Android versions. + * + * It allows you to set various properties, including border width, border color, and circle + * background color, through XML attributes or programmatically. The image scaling is fixed to + * `CENTER_CROP` to ensure the image fits within the circular frame. + * + * The view also provides options to disable the circular transformation and overlay the border + * on top of the image. + * + * Constructors: + * - `CircleImageView(Context context)`: Creates the view programmatically with default settings. + * - `CircleImageView(Context context, AttributeSet attrs, int defStyle)`: Creates the view + * programmatically with attributes specified in XML. + */ +class CircleImageView : AppCompatImageView { + companion object { + /** + * The default scale type for the image, ensuring it fits within the circular frame. + */ + private val SCALE_TYPE: ScaleType = ScaleType.CENTER_CROP + /** + * The bitmap configuration for images, using ARGB_8888 for high-quality images. + */ + private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 + /** + * The default dimension for `ColorDrawable` instances. + */ + private const val COLORDRAWABLE_DIMENSION = 2 + /** + * The default border width in pixels. + */ + private const val DEFAULT_BORDER_WIDTH = 0 + /** + * The default border color, set to black. + */ + private const val DEFAULT_BORDER_COLOR = Color.BLACK + /** + * The default background color for the circle, set to transparent. + */ + private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT + /** + * The default image alpha, set to fully opaque. + */ + private const val DEFAULT_IMAGE_ALPHA = 255 + /** + * The default setting for border overlay, which determines whether the border is drawn + * on top of the image. + */ + private const val DEFAULT_BORDER_OVERLAY = false + } + + private val mDrawableRect = RectF() + private val mBorderRect = RectF() + + private val mShaderMatrix = Matrix() + private val mBitmapPaint = Paint() + private val mBorderPaint = Paint() + private val mCircleBackgroundPaint = Paint() + + private var mBorderColor = DEFAULT_BORDER_COLOR + private var mBorderWidth = DEFAULT_BORDER_WIDTH + private var mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR + private var mImageAlpha = DEFAULT_IMAGE_ALPHA + + private var mBitmap: Bitmap? = null + private var mBitmapCanvas: Canvas? = null + + private var mDrawableRadius = 0f + private var mBorderRadius = 0f + + private var mColorFilter: ColorFilter? = null + + private var mInitialized = false + private var mRebuildShader = false + private var mDrawableDirty = false + + private var mBorderOverlay = false + private var mDisableCircularTransformation = false + /** + * Constructor that initializes the view programmatically. + * + * @param context The context of the application. + */ + constructor(context: Context?) : super(context!!) { + init() + } + /** + * Constructor that initializes the view with attributes specified in XML. + * + * @param context The context of the application. + * @param attrs The attribute set containing the XML attributes. + * @param defStyle The default style to apply to this view. + */ + @JvmOverloads + constructor(context: Context, attrs: AttributeSet?, defStyle: Int = 0) + : super(context, attrs, defStyle) { + val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0) + + mBorderWidth = a.getDimensionPixelSize( + R.styleable.CircleImageView_civ_border_width, + DEFAULT_BORDER_WIDTH + ) + mBorderColor = + a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR) + mBorderOverlay = + a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY) + mCircleBackgroundColor = a.getColor( + R.styleable.CircleImageView_civ_circle_background_color, + DEFAULT_CIRCLE_BACKGROUND_COLOR + ) + + a.recycle() + + init() + } + /** + * Initializes the custom view by setting up paint objects and scale type. + */ + private fun init() { + mInitialized = true + + super.setScaleType(SCALE_TYPE) + + mBitmapPaint.isAntiAlias = true + mBitmapPaint.isDither = true + mBitmapPaint.isFilterBitmap = true + mBitmapPaint.alpha = mImageAlpha + mBitmapPaint.setColorFilter(mColorFilter) + + mBorderPaint.style = Paint.Style.STROKE + mBorderPaint.isAntiAlias = true + mBorderPaint.color = mBorderColor + mBorderPaint.strokeWidth = mBorderWidth.toFloat() + + mCircleBackgroundPaint.style = Paint.Style.FILL + mCircleBackgroundPaint.isAntiAlias = true + mCircleBackgroundPaint.color = mCircleBackgroundColor + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + outlineProvider = + OutlineProvider() + } + } + /** + * Sets the scale type of the view. Only `CENTER_CROP` is supported. + * + * @param scaleType The scale type to set. + */ + override fun setScaleType(scaleType: ScaleType) { + if (scaleType != SCALE_TYPE) { + throw IllegalArgumentException("ScaleType $scaleType not supported.") + } + } + /** + * Overrides the ability to adjust view bounds, which is not supported in this view. + * + * @param adjustViewBounds Should always be false. + */ + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + require(!adjustViewBounds) { "adjustViewBounds not supported." } + } + /** + * Draws the circular image, background, and border. + * + * @param canvas The canvas on which to draw. + */ + @SuppressLint("CanvasSize") + override fun onDraw(canvas: Canvas) { + if (mDisableCircularTransformation) { + super.onDraw(canvas) + return + } + + if (mCircleBackgroundColor != Color.TRANSPARENT) { + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mCircleBackgroundPaint + ) + } + + if (mBitmap != null) { + if (mDrawableDirty && mBitmapCanvas != null) { + mDrawableDirty = false + val drawable = drawable + drawable.setBounds(0, 0, mBitmapCanvas!!.width, mBitmapCanvas!!.height) + drawable.draw(mBitmapCanvas!!) + } + + if (mRebuildShader) { + mRebuildShader = false + + val bitmapShader = + BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + bitmapShader.setLocalMatrix(mShaderMatrix) + + mBitmapPaint.setShader(bitmapShader) + } + + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mBitmapPaint + ) + } + + if (mBorderWidth > 0) { + canvas.drawCircle( + mBorderRect.centerX(), + mBorderRect.centerY(), + mBorderRadius, + mBorderPaint + ) + } + } + /** + * Marks the drawable as dirty, indicating it needs to be redrawn. + * + * @param dr The drawable that is being invalidated. + */ + override fun invalidateDrawable(dr: Drawable) { + mDrawableDirty = true + invalidate() + } + /** + * Updates the dimensions when the size of the view changes. + * + * @param w The new width of the view. + * @param h The new height of the view. + * @param oldw The old width of the view. + * @param oldh The old height of the view. + */ + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + updateDimensions() + invalidate() + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(left, top, right, bottom) + updateDimensions() + invalidate() + } + + override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { + super.setPaddingRelative(start, top, end, bottom) + updateDimensions() + invalidate() + } + + /** + * Sets the border color for the circular image. + * + * @param borderColor The color of the border. + */ + var borderColor: Int + get() = mBorderColor + set(borderColor) { + if (borderColor == mBorderColor) { + return + } + + mBorderColor = borderColor + mBorderPaint.color = borderColor + invalidate() + } + /** + * Sets the background color for the circular image. + * + * @param circleBackgroundColor The background color for the circle. + */ + var circleBackgroundColor: Int + get() = mCircleBackgroundColor + set(circleBackgroundColor) { + if (circleBackgroundColor == mCircleBackgroundColor) { + return + } + + mCircleBackgroundColor = circleBackgroundColor + mCircleBackgroundPaint.color = circleBackgroundColor + invalidate() + } + + + @Deprecated("Use {@link #setCircleBackgroundColor(int)} instead") + fun setCircleBackgroundColorResource(@ColorRes circleBackgroundRes: Int) { + circleBackgroundColor = context.resources.getColor(circleBackgroundRes, context.theme) + } + /** + * Sets the border width for the circular image. + * + * @param borderWidth The width of the border in pixels. + */ + var borderWidth: Int + get() = mBorderWidth + set(borderWidth) { + if (borderWidth == mBorderWidth) { + return + } + + mBorderWidth = borderWidth + mBorderPaint.strokeWidth = borderWidth.toFloat() + updateDimensions() + invalidate() + } + + var isBorderOverlay: Boolean + get() = mBorderOverlay + set(borderOverlay) { + if (borderOverlay == mBorderOverlay) { + return + } + + mBorderOverlay = borderOverlay + updateDimensions() + invalidate() + } + + var isDisableCircularTransformation: Boolean + get() = mDisableCircularTransformation + set(disableCircularTransformation) { + if (disableCircularTransformation == mDisableCircularTransformation) { + return + } + + mDisableCircularTransformation = disableCircularTransformation + + if (disableCircularTransformation) { + mBitmap = null + mBitmapCanvas = null + mBitmapPaint.setShader(null) + } else { + initializeBitmap() + } + + invalidate() + } + + override fun setImageBitmap(bm: Bitmap) { + super.setImageBitmap(bm) + initializeBitmap() + invalidate() + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + initializeBitmap() + invalidate() + } + + override fun setImageResource(@DrawableRes resId: Int) { + super.setImageResource(resId) + initializeBitmap() + invalidate() + } + + override fun setImageURI(uri: Uri?) { + super.setImageURI(uri) + initializeBitmap() + invalidate() + } + + override fun setImageAlpha(alpha: Int) { + val newAlpha = alpha and 0xFF + + if (newAlpha == mImageAlpha) { + return + } + + mImageAlpha = newAlpha + + if (mInitialized) { + mBitmapPaint.alpha = newAlpha + invalidate() + } + } + + override fun getImageAlpha(): Int { + return mImageAlpha + } + + override fun setColorFilter(cf: ColorFilter) { + if (cf === mColorFilter) { + return + } + + mColorFilter = cf + + // This might be called during ImageView construction before + // member initialization has finished on API level <= 19. + if (mInitialized) { + mBitmapPaint.setColorFilter(cf) + invalidate() + } + } + + override fun getColorFilter(): ColorFilter { + return mColorFilter!! + } + + private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? { + if (drawable == null) { + return null + } + + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + try { + val bitmap = if (drawable is ColorDrawable) { + Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG) + } else { + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + BITMAP_CONFIG + ) + } + + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + private fun initializeBitmap() { + mBitmap = getBitmapFromDrawable(drawable) + + mBitmapCanvas = if (mBitmap != null && mBitmap!!.isMutable) { + Canvas(mBitmap!!) + } else { + null + } + + if (!mInitialized) { + return + } + + if (mBitmap != null) { + updateShaderMatrix() + } else { + mBitmapPaint.setShader(null) + } + } + + private fun updateDimensions() { + mBorderRect.set(calculateBounds()) + mBorderRadius = min( + ((mBorderRect.height() - mBorderWidth) / 2.0f).toDouble(), + ((mBorderRect.width() - mBorderWidth) / 2.0f).toDouble() + ).toFloat() + + mDrawableRect.set(mBorderRect) + if (!mBorderOverlay && mBorderWidth > 0) { + mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f) + } + mDrawableRadius = min( + (mDrawableRect.height() / 2.0f).toDouble(), + (mDrawableRect.width() / 2.0f).toDouble() + ).toFloat() + + updateShaderMatrix() + } + + private fun calculateBounds(): RectF { + val availableWidth = width - paddingLeft - paddingRight + val availableHeight = height - paddingTop - paddingBottom + + val sideLength = min(availableWidth.toDouble(), availableHeight.toDouble()) + .toInt() + + val left = paddingLeft + (availableWidth - sideLength) / 2f + val top = paddingTop + (availableHeight - sideLength) / 2f + + return RectF(left, top, left + sideLength, top + sideLength) + } + + private fun updateShaderMatrix() { + if (mBitmap == null) { + return + } + + val scale: Float + var dx = 0f + var dy = 0f + + mShaderMatrix.set(null) + + val bitmapHeight = mBitmap!!.height + val bitmapWidth = mBitmap!!.width + + if (bitmapWidth * mDrawableRect.height() > mDrawableRect.width() * bitmapHeight) { + scale = mDrawableRect.height() / bitmapHeight.toFloat() + dx = (mDrawableRect.width() - bitmapWidth * scale) * 0.5f + } else { + scale = mDrawableRect.width() / bitmapWidth.toFloat() + dy = (mDrawableRect.height() - bitmapHeight * scale) * 0.5f + } + + mShaderMatrix.setScale(scale, scale) + mShaderMatrix.postTranslate( + (dx + 0.5f).toInt() + mDrawableRect.left, + (dy + 0.5f).toInt() + mDrawableRect.top + ) + + mRebuildShader = true + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + if (mDisableCircularTransformation) { + return super.onTouchEvent(event) + } + + return inTouchableArea(event.x, event.y) && super.onTouchEvent(event) + } + + private fun inTouchableArea(x: Float, y: Float): Boolean { + if (mBorderRect.isEmpty) { + return true + } + + return ((x - mBorderRect.centerX()).toDouble().pow(2.0) + + (y - mBorderRect.centerY()).toDouble().pow(2.0) <= + mBorderRadius.toDouble().pow(2.0)) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private inner class OutlineProvider : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + if (mDisableCircularTransformation) { + BACKGROUND.getOutline(view, outline) + } else { + val bounds = Rect() + mBorderRect.roundOut(bounds) + outline.setRoundRect(bounds, bounds.width() / 2.0f) + } + } + } + + +} diff --git a/app/src/main/java/com/banrossyn/circleviewimagektx/MainActivity.kt b/app/src/main/java/com/banrossyn/circleviewimagektx/MainActivity.kt new file mode 100644 index 0000000..5ea4a92 --- /dev/null +++ b/app/src/main/java/com/banrossyn/circleviewimagektx/MainActivity.kt @@ -0,0 +1,36 @@ +package com.banrossyn.circleviewimagektx + +import android.graphics.Color +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +class MainActivity : AppCompatActivity() { + + + override fun onCreate(savedInstanceState : Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_main) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + // In your Activity or Fragment + val profileImageView = findViewById(R.id.profile_img) + + // Set border color programmatically + // profileImageView.borderColor = Color.RED + + // Set border width programmatically + // profileImageView.borderWidth = 5 // in pixels + + // Set circle background color programmatically + // profileImageView.circleBackgroundColor = Color.BLUE + + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/border.xml b/app/src/main/res/drawable/border.xml new file mode 100644 index 0000000..34a87b8 --- /dev/null +++ b/app/src/main/res/drawable/border.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/src_pic.png b/app/src/main/res/drawable/src_pic.png new file mode 100644 index 0000000..d78ef2f Binary files /dev/null and b/app/src/main/res/drawable/src_pic.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..6af73d0 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..83d6d71 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..6d81bd5 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..6103165 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,150 @@ + + + #FF000000 + #FFFFFFFF + + + #2196F3 + #FFFFFF + #3CA3FF + #001226 + #416084 + #FFFFFF + #BEDAFF + #224365 + #873EA4 + #FFFFFF + #CD7FEA + #210030 + #BA1A1A + #FFFFFF + #FFDAD6 + #410002 + #F8F9FF + #181C22 + #F8F9FF + #4B181C22 + #181C22 + #DBE3F0 + #404752 + #707883 + #BFC7D4 + #000000 + #33000000 + #2D3137 + #EEF1F9 + #9ECAFF + #D1E4FF + #001D36 + #9ECAFF + #00497D + #D1E4FF + #001D36 + #A9C9F2 + #28496B + #F9D8FF + #320046 + #ECB1FF + #6D238A + #D7DAE2 + #F8F9FF + #FFFFFF + #F1F3FC + #EBEEF6 + #E5E8F0 + #DFE2EA + #004576 + #FFFFFF + #0078C8 + #FFFFFF + #244567 + #FFFFFF + #58779C + #FFFFFF + #681D86 + #FFFFFF + #A055BD + #FFFFFF + #8C0009 + #FFFFFF + #DA342E + #FFFFFF + #F8F9FF + #181C22 + #F8F9FF + #181C22 + #DBE3F0 + #3C434E + #58606B + #737B87 + #000000 + #2D3137 + #EEF1F9 + #9ECAFF + #0078C8 + #FFFFFF + #005FA0 + #FFFFFF + #58779C + #FFFFFF + #3F5E82 + #FFFFFF + #A055BD + #FFFFFF + #843BA2 + #FFFFFF + #D7DAE2 + #F8F9FF + #FFFFFF + #F1F3FC + #EBEEF6 + #E5E8F0 + #DFE2EA + #002341 + #FFFFFF + #004576 + #FFFFFF + #002341 + #FFFFFF + #244567 + #FFFFFF + #3C0054 + #FFFFFF + #681D86 + #FFFFFF + #4E0002 + #FFFFFF + #8C0009 + #FFFFFF + #F8F9FF + #181C22 + #F8F9FF + #000000 + #DBE3F0 + #1D242E + #3C434E + #3C434E + #000000 + #2D3137 + #FFFFFF + #E2EDFF + #004576 + #FFFFFF + #002E52 + #FFFFFF + #244567 + #FFFFFF + #072E4F + #FFFFFF + #681D86 + #FFFFFF + #4D0069 + #FFFFFF + #D7DAE2 + #F8F9FF + #FFFFFF + #F1F3FC + #EBEEF6 + #E5E8F0 + #DFE2EA + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..cb6adbf --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + CircleViewImage.ktx + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..f6eb986 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +