diff --git a/app/build.gradle b/app/build.gradle index 0ceae1a1..b071c1ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,6 +181,7 @@ dependencies { implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2" // Szkolny.eu libraries/forks + implementation project(":navlib") implementation "eu.szkolny:android-snowfall:1ca9ea2da3" implementation "eu.szkolny:agendacalendarview:1.0.4" implementation "eu.szkolny:cafebar:5bf0c618de" @@ -191,7 +192,6 @@ dependencies { implementation "eu.szkolny.selective-dao:annotation:27f8f3f194" officialImplementation "eu.szkolny:ssl-provider:1.0.0" unofficialImplementation "eu.szkolny:ssl-provider:1.0.0" - implementation "pl.szczodrzynski:navlib:0.8.0" implementation "pl.szczodrzynski:numberslidingpicker:2921225f76" implementation "pl.szczodrzynski:recyclertablayout:700f980584" implementation "pl.szczodrzynski:tachyon:551943a6b5" diff --git a/navlib-font/build.gradle b/navlib-font/build.gradle new file mode 100644 index 00000000..7f254181 --- /dev/null +++ b/navlib-font/build.gradle @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Mike Penz + * + * 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. + */ + +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +android { + compileSdkVersion setup.compileSdk + + defaultConfig { + minSdkVersion setup.minSdk + targetSdkVersion setup.targetSdk + consumerProguardFiles 'consumer-proguard-rules.pro' + versionCode 10 + versionName "1.0" + + resValue "string", "NavLibFont_version", "${versionName}" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation "com.mikepenz:iconics-typeface-api:5.3.0-b01" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" +} diff --git a/navlib-font/consumer-proguard-rules.pro b/navlib-font/consumer-proguard-rules.pro new file mode 100644 index 00000000..ef3e42a0 --- /dev/null +++ b/navlib-font/consumer-proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.mikepenz.iconics.typeface.library.navlibfont.NavLibFont { *; } diff --git a/navlib-font/gradle.properties b/navlib-font/gradle.properties new file mode 100644 index 00000000..163aa9f2 --- /dev/null +++ b/navlib-font/gradle.properties @@ -0,0 +1,18 @@ +# +# Copyright 2019 Mike Penz +# +# 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. +# +POM_NAME=Android-Iconics NavLibFont Typeface Library +POM_ARTIFACT_ID=navlibfont-typeface +POM_PACKAGING=aar diff --git a/navlib-font/src/main/AndroidManifest.xml b/navlib-font/src/main/AndroidManifest.xml new file mode 100644 index 00000000..edcc9806 --- /dev/null +++ b/navlib-font/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/navlib-font/src/main/java/com/mikepenz/iconics/typeface/library/navlibfont/NavLibFont.kt b/navlib-font/src/main/java/com/mikepenz/iconics/typeface/library/navlibfont/NavLibFont.kt new file mode 100644 index 00000000..822fde52 --- /dev/null +++ b/navlib-font/src/main/java/com/mikepenz/iconics/typeface/library/navlibfont/NavLibFont.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Mike Penz + * + * 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.mikepenz.iconics.typeface.library.navlibfont + +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.ITypeface +import java.util.LinkedList + +@Suppress("EnumEntryName") +object NavLibFont : ITypeface { + + override val fontRes: Int + get() = R.font.navlibfont_font_v1_0 + + override val characters: Map by lazy { + Icon.values().associate { it.name to it.character } + } + + override val mappingPrefix: String + get() = "nav" + + override val fontName: String + get() = "NavLibFont" + + override val version: String + get() = "1.0" + + override val iconCount: Int + get() = characters.size + + override val icons: List + get() = characters.keys.toCollection(LinkedList()) + + override val author: String + get() = "Kuba SzczodrzyƄski" + + override val url: String + get() = "https://github.com/kuba2k2/NavLib" + + override val description: String + get() = "" + + override val license: String + get() = "" + + override val licenseUrl: String + get() = "" + + override fun getIcon(key: String): IIcon = Icon.valueOf(key) + + enum class Icon constructor(override val character: Char) : IIcon { + nav_dots_vertical('\ue801'), + nav_menu('\ue800'), + nav_sort_ascending('\ue803'), + nav_sort_descending('\ue802'); + + override val typeface: ITypeface by lazy { NavLibFont } + } +} \ No newline at end of file diff --git a/navlib-font/src/main/res/font/navlibfont_font_v1_0.ttf b/navlib-font/src/main/res/font/navlibfont_font_v1_0.ttf new file mode 100644 index 00000000..f0da0117 Binary files /dev/null and b/navlib-font/src/main/res/font/navlibfont_font_v1_0.ttf differ diff --git a/navlib-font/src/main/res/values/font_addon.xml b/navlib-font/src/main/res/values/font_addon.xml new file mode 100644 index 00000000..1d094be3 --- /dev/null +++ b/navlib-font/src/main/res/values/font_addon.xml @@ -0,0 +1,20 @@ + + + + + com.mikepenz.iconics.typeface.library.navlibfont.NavLibFont + diff --git a/navlib-font/src/main/res/values/font_description.xml b/navlib-font/src/main/res/values/font_description.xml new file mode 100644 index 00000000..1421308b --- /dev/null +++ b/navlib-font/src/main/res/values/font_description.xml @@ -0,0 +1,31 @@ + + + + + year;author;libraryName;libraryWebsite + Kuba SzczodrzyƄski + https://github.com/kuba2k2/NavLib + NavLibFont + + https://github.com/kuba2k2/NavLib + @string/NavLibFont_version + + true + https://github.com/kuba2k2/NavLib + + 2018 + diff --git a/navlib/build.gradle b/navlib/build.gradle new file mode 100644 index 00000000..b935281b --- /dev/null +++ b/navlib/build.gradle @@ -0,0 +1,55 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion setup.compileSdk + + defaultConfig { + minSdkVersion setup.minSdk + targetSdkVersion setup.targetSdk + versionCode release.versionCode + versionName release.versionName + + consumerProguardFiles 'consumer-rules.pro' + + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + dataBinding = true + } + + packagingOptions { + exclude 'META-INF/library-core_release.kotlin_module' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.core:core-ktx:1.3.2" + implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "com.google.android.material:material:1.3.0" + + api "com.mikepenz:materialize:1.2.1" + api "com.mikepenz:materialdrawer:8.3.3" + api "com.mikepenz:iconics-core:5.3.0-b01" + api "com.mikepenz:itemanimators:1.1.0" + + compileOnly "pl.droidsonroids.gif:android-gif-drawable:1.2.15" + + implementation "com.balysv:material-ripple:1.0.2" + + implementation project(":navlib-font") +} diff --git a/navlib/consumer-rules.pro b/navlib/consumer-rules.pro new file mode 100644 index 00000000..619be6dd --- /dev/null +++ b/navlib/consumer-rules.pro @@ -0,0 +1 @@ +-keep class androidx.drawerlayout.widget.DrawerLayout { *; } diff --git a/navlib/proguard-rules.pro b/navlib/proguard-rules.pro new file mode 100644 index 00000000..06b02c6a --- /dev/null +++ b/navlib/proguard-rules.pro @@ -0,0 +1,23 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in init.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 + +-keep class androidx.drawerlayout.widget.DrawerLayout { *; } diff --git a/navlib/src/main/AndroidManifest.xml b/navlib/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7b521725 --- /dev/null +++ b/navlib/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/Anim.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/Anim.kt new file mode 100644 index 00000000..5b57e48f --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/Anim.kt @@ -0,0 +1,170 @@ +package pl.szczodrzynski.navlib + +import android.view.View +import android.view.ViewGroup +import android.view.animation.AccelerateInterpolator +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.DecelerateInterpolator +import android.view.animation.ScaleAnimation +import android.view.animation.Transformation +import android.widget.LinearLayout + +object Anim { + fun expand(v: View, duration: Int?, animationListener: Animation.AnimationListener?) { + v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + val targetHeight = v.measuredHeight + //Log.d("Anim", "targetHeight="+targetHeight); + v.visibility = View.VISIBLE + v.layoutParams.height = 0 + val a = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + v.layoutParams.height = if (interpolatedTime == 1.0f) + LinearLayout.LayoutParams.WRAP_CONTENT//(int)(targetHeight * interpolatedTime) + else + (targetHeight * interpolatedTime).toInt() + v.requestLayout() + } + + override fun willChangeBounds(): Boolean { + return true + } + } + if (duration == null) { + a.duration = (targetHeight.toFloat() / v.context.resources.displayMetrics.density).toInt().toLong() + } else { + a.duration = duration as Long + } + if (animationListener != null) { + a.setAnimationListener(animationListener) + } + v.startAnimation(a) + } + + fun collapse(v: View, duration: Int?, animationListener: Animation.AnimationListener?) { + val initialHeight = v.measuredHeight + val a = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + if (interpolatedTime == 1.0f) { + v.visibility = View.GONE + return + } + v.layoutParams.height = initialHeight - (initialHeight.toFloat() * interpolatedTime).toInt() + v.requestLayout() + } + + override fun willChangeBounds(): Boolean { + return true + } + } + if (duration == null) { + a.duration = (initialHeight.toFloat() / v.context.resources.displayMetrics.density).toInt().toLong() + } else { + a.duration = duration as Long + } + if (animationListener != null) { + a.setAnimationListener(animationListener) + } + v.startAnimation(a) + } + + fun fadeIn(v: View, duration: Int?, animationListener: Animation.AnimationListener?) { + val fadeIn = AlphaAnimation(0f, 1f) + fadeIn.interpolator = DecelerateInterpolator() //add this + fadeIn.duration = duration!!.toLong() + fadeIn.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + v.visibility = View.VISIBLE + animationListener?.onAnimationStart(animation) + } + + override fun onAnimationEnd(animation: Animation) { + animationListener?.onAnimationEnd(animation) + } + + override fun onAnimationRepeat(animation: Animation) { + animationListener?.onAnimationRepeat(animation) + } + }) + v.startAnimation(fadeIn) + } + + fun fadeOut(v: View, duration: Int?, animationListener: Animation.AnimationListener?) { + val fadeOut = AlphaAnimation(1f, 0f) + fadeOut.interpolator = AccelerateInterpolator() //and this + fadeOut.duration = duration!!.toLong() + fadeOut.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + animationListener?.onAnimationStart(animation) + } + + override fun onAnimationEnd(animation: Animation) { + v.visibility = View.INVISIBLE + animationListener?.onAnimationEnd(animation) + } + + override fun onAnimationRepeat(animation: Animation) { + animationListener?.onAnimationRepeat(animation) + } + }) + v.startAnimation(fadeOut) + } + + fun scaleView( + v: View, + duration: Int?, + animationListener: Animation.AnimationListener?, + startScale: Float, + endScale: Float + ) { + val anim = ScaleAnimation( + 1f, 1f, // Start and end values for the X axis scaling + startScale, endScale, // Start and end values for the Y axis scaling + Animation.RELATIVE_TO_SELF, 0f, // Pivot point of X scaling + Animation.RELATIVE_TO_SELF, 0f + ) // Pivot point of Y scaling + anim.fillAfter = true // Needed to keep the result of the animation + anim.duration = duration!!.toLong() + anim.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + animationListener?.onAnimationStart(animation) + } + + override fun onAnimationEnd(animation: Animation) { + animationListener?.onAnimationEnd(animation) + } + + override fun onAnimationRepeat(animation: Animation) { + animationListener?.onAnimationRepeat(animation) + } + }) + v.startAnimation(anim) + } + + class ResizeAnimation( + private val mView: View, + private val mFromWidth: Float, + private val mFromHeight: Float, + private val mToWidth: Float, + private val mToHeight: Float + ) : Animation() { + + private val width: Float + private val height: Float + + init { + width = mView.width.toFloat() + height = mView.height.toFloat() + duration = 300 + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + val height = (mToHeight - mFromHeight) * interpolatedTime + mFromHeight + val width = (mToWidth - mFromWidth) * interpolatedTime + mFromWidth + val p = mView.layoutParams + p.width = (width * this.width).toInt() + p.height = (height * this.height).toInt() + mView.requestLayout() + } + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/BadgeDrawable.java b/navlib/src/main/java/pl/szczodrzynski/navlib/BadgeDrawable.java new file mode 100644 index 00000000..c47c3ff2 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/BadgeDrawable.java @@ -0,0 +1,109 @@ +package pl.szczodrzynski.navlib; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; + +import androidx.core.content.ContextCompat; + +public class BadgeDrawable extends Drawable { + + private Paint mBadgePaint; + private Paint mBadgePaint1; + private Paint mTextPaint; + private Rect mTxtRect = new Rect(); + + private String mCount = ""; + private boolean mWillDraw = false; + + public BadgeDrawable(Context context) { + float mTextSize = context.getResources().getDimension(R.dimen.badge_text_size); + + mBadgePaint = new Paint(); + mBadgePaint.setColor(0xffff3d00); + mBadgePaint.setAntiAlias(true); + mBadgePaint.setStyle(Paint.Style.FILL); + /*mBadgePaint1 = new Paint(); + mBadgePaint1.setColor(ContextCompat.getColor(context.getApplicationContext(), R.color.grey_ivory5)); + mBadgePaint1.setAntiAlias(true); + mBadgePaint1.setStyle(Paint.Style.FILL);*/ + + mTextPaint = new Paint(); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setTypeface(Typeface.DEFAULT); + mTextPaint.setTextSize(mTextSize); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextAlign(Paint.Align.CENTER); + } + + @Override + public void draw(Canvas canvas) { + + + + if (!mWillDraw) { + return; + } + Rect bounds = getBounds(); + float width = bounds.right - bounds.left; + float height = bounds.bottom - bounds.top; + + // Position the badge in the top-right quadrant of the icon. + + /*Using Math.max rather than Math.min */ + + float radius = ((Math.max(width, height) / 2)) / 2; + float centerX = (width - radius - 1) +5; + float centerY = radius -5; + if(mCount.length() <= 2){ + // Draw badge circle. + //canvas.drawCircle(centerX, centerY, (int)(radius+7.5), mBadgePaint1); + canvas.drawCircle(centerX, centerY, (int)(radius+5.5), mBadgePaint); + } + else{ + //canvas.drawCircle(centerX, centerY, (int)(radius+8.5), mBadgePaint1); + canvas.drawCircle(centerX, centerY, (int)(radius+6.5), mBadgePaint); +// canvas.drawRoundRect(radius, radius, radius, radius, 10, 10, mBadgePaint); + } + // Draw badge count text inside the circle. + mTextPaint.getTextBounds(mCount, 0, mCount.length(), mTxtRect); + float textHeight = mTxtRect.bottom - mTxtRect.top; + float textY = centerY + (textHeight / 2f); + if(mCount.length() > 2) + canvas.drawText("99+", centerX, textY, mTextPaint); + else + canvas.drawText(mCount, centerX, textY, mTextPaint); + } + + /* + Sets the count (i.e notifications) to display. + */ + public void setCount(String count) { + mCount = count; + + // Only draw a badge if there are notifications. + mWillDraw = !count.equalsIgnoreCase("0"); + invalidateSelf(); + } + + @Override + public void setAlpha(int alpha) { + // do nothing + } + + @Override + public void setColorFilter(ColorFilter cf) { + // do nothing + } + + @Override + public int getOpacity() { + return PixelFormat.UNKNOWN; + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/BezelGifImageView.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/BezelGifImageView.kt new file mode 100644 index 00000000..716ee1be --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/BezelGifImageView.kt @@ -0,0 +1,278 @@ +package pl.szczodrzynski.navlib + +import android.annotation.TargetApi +import android.content.Context +import android.graphics.* +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.core.view.ViewCompat +import com.mikepenz.materialdrawer.R +import com.mikepenz.materialdrawer.util.DrawerImageLoader +import pl.droidsonroids.gif.GifImageView + + +/** + * An [android.widget.ImageView] that draws its contents inside a mask and draws a border + * drawable on top. This is useful for applying a beveled look to image contents, but is also + * flexible enough for use with other desired aesthetics. + */ +open class BezelImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : GifImageView(context, attrs, defStyle) { + private val mBlackPaint: Paint + private val mMaskedPaint: Paint + + private var mBounds: Rect? = null + private var mBoundsF: RectF? = null + + private val mMaskDrawable: Drawable? + private var mDrawCircularShadow = true + + private var mDesaturateColorFilter: ColorMatrixColorFilter? = null + + private val mSelectorAlpha = 150 + private var mSelectorColor: Int = 0 + private var mSelectorFilter: ColorFilter? = null + + private var mCacheValid = false + private var mCacheBitmap: Bitmap + private var mCachedWidth: Int = 0 + private var mCachedHeight: Int = 0 + + private var mIsPressed = false + private var mIsSelected: Boolean = false + + private var mTempDesaturateColorFilter: ColorMatrixColorFilter? = null + private var mTempSelectorFilter: ColorFilter? = null + + init { + + // Attribute initialization + val a = context.obtainStyledAttributes(attrs, R.styleable.BezelImageView, defStyle, R.style.BezelImageView) + + mMaskDrawable = a.getDrawable(R.styleable.BezelImageView_materialDrawerMaskDrawable) + if (mMaskDrawable != null) { + mMaskDrawable.callback = this + } + + mDrawCircularShadow = a.getBoolean(R.styleable.BezelImageView_materialDrawerDrawCircularShadow, true) + + mSelectorColor = a.getColor(R.styleable.BezelImageView_materialDrawerSelectorOnPress, 0) + + a.recycle() + + // Other initialization + mBlackPaint = Paint() + mBlackPaint.color = -0x1000000 + + mMaskedPaint = Paint() + mMaskedPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + + // Always want a cache allocated. + mCacheBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + + // Create a desaturate color filter for pressed state. + val cm = ColorMatrix() + cm.setSaturation(0f) + mDesaturateColorFilter = ColorMatrixColorFilter(cm) + + //create a selectorFilter if we already have a color + if (mSelectorColor != 0) { + this.mSelectorFilter = PorterDuffColorFilter(Color.argb(mSelectorAlpha, Color.red(mSelectorColor), Color.green(mSelectorColor), Color.blue(mSelectorColor)), PorterDuff.Mode.SRC_ATOP) + } + } + + override fun onSizeChanged(w: Int, h: Int, old_w: Int, old_h: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (mDrawCircularShadow) { + outlineProvider = CustomOutline(w, h) + } + } + } + + @TargetApi(21) + private inner class CustomOutline internal constructor(internal var width: Int, internal var height: Int) : ViewOutlineProvider() { + + override fun getOutline(view: View, outline: Outline) { + outline.setOval(0, 0, width, height) + } + } + + override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean { + val changed = super.setFrame(l, t, r, b) + mBounds = Rect(0, 0, r - l, b - t).also { + mBoundsF = RectF(it) + + if (mMaskDrawable != null) { + mMaskDrawable.bounds = it + } + } + + if (changed) { + mCacheValid = false + } + + return changed + } + + override fun onDraw(canvas: Canvas) { + val bounds = mBounds ?: return + + val width = bounds.width() + val height = bounds.height() + + if (width == 0 || height == 0) { + return + } + + if (!mCacheValid || width != mCachedWidth || height != mCachedHeight || mIsSelected != mIsPressed) { + // Need to redraw the cache + if (width == mCachedWidth && height == mCachedHeight) { + // Have a correct-sized bitmap cache already allocated. Just erase it. + mCacheBitmap.eraseColor(0) + } else { + // Allocate a new bitmap with the correct dimensions. + mCacheBitmap.recycle() + + mCacheBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + mCachedWidth = width + mCachedHeight = height + } + + val cacheCanvas = Canvas(mCacheBitmap) + + when { + mMaskDrawable != null -> { + val sc = cacheCanvas.save() + mMaskDrawable.draw(cacheCanvas) + if (mIsSelected) { + if (mSelectorFilter != null) { + mMaskedPaint.colorFilter = mSelectorFilter + } else { + mMaskedPaint.colorFilter = mDesaturateColorFilter + + } + } else { + mMaskedPaint.colorFilter = null + } + cacheCanvas.saveLayer(mBoundsF, mMaskedPaint, Canvas.ALL_SAVE_FLAG) + super.onDraw(cacheCanvas) + cacheCanvas.restoreToCount(sc) + } + mIsSelected -> { + val sc = cacheCanvas.save() + cacheCanvas.drawRect(0f, 0f, mCachedWidth.toFloat(), mCachedHeight.toFloat(), mBlackPaint) + if (mSelectorFilter != null) { + mMaskedPaint.colorFilter = mSelectorFilter + } else { + mMaskedPaint.colorFilter = mDesaturateColorFilter + } + cacheCanvas.saveLayer(mBoundsF, mMaskedPaint, Canvas.ALL_SAVE_FLAG) + super.onDraw(cacheCanvas) + cacheCanvas.restoreToCount(sc) + } + else -> super.onDraw(cacheCanvas) + } + } + + // Draw from cache + canvas.drawBitmap(mCacheBitmap, bounds.left.toFloat(), bounds.top.toFloat(), null) + + //remember the previous press state + mIsPressed = isPressed + } + + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + // Check for clickable state and do nothing if disabled + if (!this.isClickable) { + this.mIsSelected = false + return super.onTouchEvent(event) + } + + // Set selected state based on Motion Event + when (event.action) { + MotionEvent.ACTION_DOWN -> this.mIsSelected = true + MotionEvent.ACTION_UP, MotionEvent.ACTION_SCROLL, MotionEvent.ACTION_OUTSIDE, MotionEvent.ACTION_CANCEL -> this.mIsSelected = false + } + + // Redraw image and return super type + this.invalidate() + return super.dispatchTouchEvent(event) + } + + override fun drawableStateChanged() { + super.drawableStateChanged() + if (mMaskDrawable != null && mMaskDrawable.isStateful) { + mMaskDrawable.state = drawableState + } + if (isDuplicateParentStateEnabled) { + ViewCompat.postInvalidateOnAnimation(this) + } + } + + override fun invalidateDrawable(who: Drawable) { + if (who === mMaskDrawable) { + invalidate() + } else { + super.invalidateDrawable(who) + } + } + + override fun verifyDrawable(who: Drawable): Boolean { + return who === mMaskDrawable || super.verifyDrawable(who) + } + + + /** + * Sets the color of the selector to be draw over the + * CircularImageView. Be sure to provide some opacity. + * + * @param selectorColor The color (including alpha) to set for the selector overlay. + */ + fun setSelectorColor(selectorColor: Int) { + this.mSelectorColor = selectorColor + this.mSelectorFilter = PorterDuffColorFilter(Color.argb(mSelectorAlpha, Color.red(mSelectorColor), Color.green(mSelectorColor), Color.blue(mSelectorColor)), PorterDuff.Mode.SRC_ATOP) + this.invalidate() + } + + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + } + + override fun setImageResource(resId: Int) { + super.setImageResource(resId) + } + + override fun setImageBitmap(bm: Bitmap) { + super.setImageBitmap(bm) + } + + override fun setImageURI(uri: Uri?) { + if ("http" == uri?.scheme || "https" == uri?.scheme) { + DrawerImageLoader.instance.setImage(this, uri, null) + } else { + super.setImageURI(uri) + } + } + + fun disableTouchFeedback(disable: Boolean) { + if (disable) { + mTempDesaturateColorFilter = this.mDesaturateColorFilter + mTempSelectorFilter = this.mSelectorFilter + this.mSelectorFilter = null + this.mDesaturateColorFilter = null + } else { + if (mTempDesaturateColorFilter != null) { + this.mDesaturateColorFilter = mTempDesaturateColorFilter + } + if (mTempSelectorFilter != null) { + this.mSelectorFilter = mTempSelectorFilter + } + } + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/DrawerExtensions.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/DrawerExtensions.kt new file mode 100644 index 00000000..2ec9084e --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/DrawerExtensions.kt @@ -0,0 +1,85 @@ +package pl.szczodrzynski.navlib + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.materialdrawer.* +import com.mikepenz.materialdrawer.holder.StringHolder +import com.mikepenz.materialdrawer.model.AbstractDrawerItem +import com.mikepenz.materialdrawer.model.BaseDrawerItem +import com.mikepenz.materialdrawer.model.interfaces.* +import com.mikepenz.materialdrawer.util.getDrawerItem +import com.mikepenz.materialdrawer.util.updateItem +import com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView + +/*inline fun DrawerBuilder.withOnDrawerItemClickListener(crossinline listener: (view: View?, position: Int, drawerItem: IDrawerItem<*>) -> Boolean): DrawerBuilder { + return this.withOnDrawerItemClickListener(object : Drawer.OnDrawerItemClickListener { + override fun onItemClick(view: View?, position: Int, drawerItem: IDrawerItem<*>): Boolean { + return listener(view, position, drawerItem) + } + }) +} + +inline fun DrawerBuilder.withOnDrawerItemLongClickListener(crossinline listener: (view: View, position: Int, drawerItem: IDrawerItem<*>) -> Boolean): DrawerBuilder { + return this.withOnDrawerItemLongClickListener(object : Drawer.OnDrawerItemLongClickListener { + override fun onItemLongClick(view: View, position: Int, drawerItem: IDrawerItem<*>): Boolean { + return listener(view, position, drawerItem) + } + }) +} + +inline fun AccountHeaderBuilder.withOnAccountHeaderListener(crossinline listener: (view: View?, profile: IProfile<*>, current: Boolean) -> Boolean): AccountHeaderBuilder { + return this.withOnAccountHeaderListener(object : AccountHeader.OnAccountHeaderListener { + override fun onProfileChanged(view: View?, profile: IProfile<*>, current: Boolean): Boolean { + return listener(view, profile, current) + } + }) +} + +inline fun AccountHeaderBuilder.withOnAccountHeaderItemLongClickListener(crossinline listener: (view: View, profile: IProfile<*>, current: Boolean) -> Boolean): AccountHeaderBuilder { + return this.withOnAccountHeaderItemLongClickListener(object : AccountHeader.OnAccountHeaderItemLongClickListener { + override fun onProfileLongClick(view: View, profile: IProfile<*>, current: Boolean): Boolean { + return listener(view, profile, current) + } + }) +} + +inline fun AccountHeaderBuilder.withOnAccountHeaderProfileImageListener( + crossinline onClick: ( + view: View, + profile: IProfile<*>, + current: Boolean + ) -> Boolean, + crossinline onLongClick: ( + view: View, + profile: IProfile<*>, + current: Boolean + ) -> Boolean +): AccountHeaderBuilder { + return this.withOnAccountHeaderProfileImageListener(object : AccountHeader.OnAccountHeaderProfileImageListener { + override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean { + return onClick(view, profile, current) + } + override fun onProfileImageLongClick(view: View, profile: IProfile<*>, current: Boolean): Boolean { + return onLongClick(view, profile, current) + } + }) +} + +inline fun MiniDrawer.withOnMiniDrawerItemClickListener(crossinline listener: (view: View?, position: Int, drawerItem: IDrawerItem<*>, type: Int) -> Boolean): MiniDrawer { + return this.withOnMiniDrawerItemClickListener(object : MiniDrawer.OnMiniDrawerItemClickListener { + override fun onItemClick(view: View?, position: Int, drawerItem: IDrawerItem<*>, type: Int): Boolean { + return listener(view, position, drawerItem, type) + } + }) +}*/ + +fun MaterialDrawerSliderView.updateBadge(identifier: Long, badge: StringHolder?) { + val drawerItem = getDrawerItem(identifier) + if (drawerItem is Badgeable) { + drawerItem.withBadge(badge) + updateItem(drawerItem) + } +} + +fun T.withIcon(icon: IIcon) = withIcon(pl.szczodrzynski.navlib.ImageHolder(icon)) diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/ImageHolder.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/ImageHolder.kt new file mode 100644 index 00000000..a2075667 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/ImageHolder.kt @@ -0,0 +1,121 @@ +package pl.szczodrzynski.navlib + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import android.net.Uri +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.appcompat.content.res.AppCompatResources +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.utils.actionBar +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.materialdrawer.util.DrawerImageLoader +import pl.droidsonroids.gif.GifDrawable +import java.io.FileNotFoundException + +/** + * Created by mikepenz on 13.07.15. + */ + +open class ImageHolder : com.mikepenz.materialdrawer.holder.ImageHolder { + + constructor(@DrawableRes iconRes: Int, colorFilter: Int?) : super(iconRes) { + this.colorFilter = colorFilter + } + constructor(iicon: IIcon) : super(null as Drawable?) { + this.iicon = iicon + } + constructor() : super() + constructor(url: String) : super(url) + constructor(uri: Uri) : super(uri) + constructor(icon: Drawable?) : super(icon) + constructor(bitmap: Bitmap) : super(bitmap) + constructor(iconRes: Int) : super(iconRes) + + var iicon: IIcon? = null + @ColorInt + var colorFilter: Int? = null + var colorFilterMode: PorterDuff.Mode = PorterDuff.Mode.DST_OVER + + + /** + * sets an existing image to the imageView + * + * @param imageView + * @param tag used to identify imageViews and define different placeholders + * @return true if an image was set + */ + override fun applyTo(imageView: ImageView, tag: String?): Boolean { + val ii = iicon + + if (uri != null) { + if (uri.toString().endsWith(".gif", true)) { + imageView.setImageDrawable(GifDrawable(uri.toString())) + } + else { + val consumed = DrawerImageLoader.instance.setImage(imageView, uri!!, tag) + if (!consumed) { + imageView.setImageURI(uri) + } + } + } else if (icon != null) { + imageView.setImageDrawable(icon) + } else if (bitmap != null) { + imageView.setImageBitmap(bitmap) + } else if (iconRes != -1) { + imageView.setImageResource(iconRes) + } else if (ii != null) { + imageView.setImageDrawable(IconicsDrawable(imageView.context, ii).actionBar()) + } else { + imageView.setImageBitmap(null) + return false + } + + if (colorFilter != null) { + imageView.colorFilter = PorterDuffColorFilter(colorFilter!!, colorFilterMode) + } + + return true + } + + /** + * this only handles Drawables + * + * @param ctx + * @param iconColor + * @param tint + * @return + */ + override fun decideIcon(ctx: Context, iconColor: ColorStateList, tint: Boolean, paddingDp: Int): Drawable? { + var icon: Drawable? = icon + val ii = iicon + val uri = uri + + when { + ii != null -> icon = IconicsDrawable(ctx).apply { + this.icon = ii + colorList = iconColor + sizeDp = 24 + } + iconRes != -1 -> icon = AppCompatResources.getDrawable(ctx, iconRes) + uri != null -> try { + val inputStream = ctx.contentResolver.openInputStream(uri) + icon = Drawable.createFromStream(inputStream, uri.toString()) + } catch (e: FileNotFoundException) { + //no need to handle this + } + } + //if we got an icon AND we have auto tinting enabled AND it is no IIcon, tint it ;) + if (icon != null && tint && iicon == null) { + icon = icon.mutate() + icon.setColorFilter(iconColor.defaultColor, PorterDuff.Mode.SRC_IN) + } + return icon + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/NavBottomBar.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/NavBottomBar.kt new file mode 100644 index 00000000..6bc40157 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/NavBottomBar.kt @@ -0,0 +1,211 @@ +package pl.szczodrzynski.navlib + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.LayerDrawable +import android.util.AttributeSet +import android.view.Gravity +import android.view.MenuItem +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat +import com.google.android.material.bottomappbar.BottomAppBar +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.navlibfont.NavLibFont +import com.mikepenz.iconics.utils.sizeDp +import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet +import pl.szczodrzynski.navlib.drawer.NavDrawer + +class NavBottomBar : BottomAppBar { + constructor(context: Context) : super(context) { + create(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + create(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { + create(attrs, defStyle) + } + + var drawer: NavDrawer? = null + var bottomSheet: NavBottomSheet? = null + var fabView: FloatingActionButton? = null + var fabExtendedView: ExtendedFloatingActionButton? = null + + /** + * Shows the BottomAppBar and sets the contentView's margin to be + * above the BottomAppBar. + */ + var enable = true + set(value) { + field = value + visibility = if (value) View.VISIBLE else View.GONE + setFabParams() + } + /** + * Whether the FAB should be visible. + */ + var fabEnable = true + set(value) { + field = value + setFabVisibility() + } + /** + * Whether an ExtendableFloatingActionButton should be used + * instead of a normal FloatingActionButton. + * Note that the extendable button does not support end alignment/gravity + * when used together with the bottom app bar. + */ + var fabExtendable = true + set(value) { + field = value + setFabParams() + } + /** + * If BottomAppBar is enabled, sets its fabAlignmentMode. + * Else, sets the actual FAB's gravity. + */ + var fabGravity = Gravity.CENTER + set(value) { + field = value + setFabParams() + } + /** + * Whether the FAB should be extended and its text visible. + */ + var fabExtended = false + set(value) { + field = value + if (fabExtended) + fabExtendedView?.extend() + else + fabExtendedView?.shrink() + } + /** + * Set the FAB's icon. + */ + var fabIcon: IIcon? = null + set(value) { + field = value + fabView?.setImageDrawable(IconicsDrawable(context).apply { + icon = value + colorAttr(context, R.attr.colorFabIcon) + sizeDp = 24 + }) + fabExtendedView?.icon = IconicsDrawable(context).apply { + icon = value + colorAttr(context, R.attr.colorFabIcon) + sizeDp = 24 + } + } + /** + * Set the ExtendedFAB's text. + */ + var fabExtendedText + get() = fabExtendedView?.text + set(value) { + fabExtendedView?.text = value + } + + /** + * Set the FAB's on click listener + */ + fun setFabOnClickListener(onClickListener: OnClickListener?) { + fabView?.setOnClickListener(onClickListener) + fabExtendedView?.setOnClickListener(onClickListener) + } + + @SuppressLint("ClickableViewAccessibility") + private fun create(attrs: AttributeSet?, defStyle: Int) { + setOnTouchListener { _, event -> + if (bottomSheet?.enable != true || bottomSheet?.enableDragToOpen != true) + return@setOnTouchListener false + bottomSheet?.dispatchBottomBarEvent(event) + true + } + + elevation = 0f + + val icon = ContextCompat.getDrawable(context, R.drawable.ic_menu_badge) as LayerDrawable? + icon?.apply { + mutate() + setDrawableByLayerId(R.id.ic_menu, IconicsDrawable(context).apply { + this.icon = NavLibFont.Icon.nav_menu + sizeDp = 24 + colorAttr(context, R.attr.colorOnPrimary) + }) + setDrawableByLayerId(R.id.ic_badge, BadgeDrawable(context)) + } + navigationIcon = icon + + menu.add(0, -1, 0, "Menu") + .setIcon(IconicsDrawable(context).apply { + this.icon = NavLibFont.Icon.nav_dots_vertical + sizeDp = 24 + colorAttr(context, R.attr.colorOnPrimary) + }) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + + setNavigationOnClickListener { + drawer?.toggle() + } + + super.setOnMenuItemClickListener { + if (it.itemId == -1 && bottomSheet?.enable == true) { + bottomSheet?.toggle() + } + else { + onMenuItemClickListener?.onMenuItemClick(it) + } + true + } + } + + private fun setFabParams() { + val layoutParams = + ((if (fabExtendable) fabExtendedView?.layoutParams else fabView?.layoutParams) ?: return) as CoordinatorLayout.LayoutParams + + if (enable) { + layoutParams.anchorId = this.id + if (fabExtendable) + layoutParams.anchorGravity = if (fabExtendable) fabGravity or Gravity.TOP else Gravity.NO_GRAVITY + layoutParams.gravity = Gravity.NO_GRAVITY + } + else { + layoutParams.anchorId = View.NO_ID + if (fabExtendable) + layoutParams.anchorGravity = Gravity.NO_GRAVITY + layoutParams.gravity = fabGravity or Gravity.BOTTOM + } + fabAlignmentMode = if (fabGravity == Gravity.END) FAB_ALIGNMENT_MODE_END else FAB_ALIGNMENT_MODE_CENTER + if (fabExtendable) + fabExtendedView?.layoutParams = layoutParams + else + fabView?.layoutParams = layoutParams + setFabVisibility() + } + private fun setFabVisibility() { + if (fabEnable && fabExtendable) { + fabView?.hide() + fabExtendedView?.show() + } + else if (fabEnable) { + fabView?.show() + fabExtendedView?.hide() + } + else { + fabView?.hide() + fabExtendedView?.hide() + } + } + + private var onMenuItemClickListener: OnMenuItemClickListener? = null + override fun setOnMenuItemClickListener(listener: OnMenuItemClickListener?) { + onMenuItemClickListener = listener + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/NavToolbar.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/NavToolbar.kt new file mode 100644 index 00000000..1bc88813 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/NavToolbar.kt @@ -0,0 +1,55 @@ +package pl.szczodrzynski.navlib + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import com.google.android.material.appbar.MaterialToolbar + +class NavToolbar : MaterialToolbar { + + constructor(context: Context) : super(context) { + create(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + create(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { + create(attrs, defStyle) + } + + var toolbarImage: ImageView? = null + set(value) { + field = value + toolbarImage?.setOnClickListener { + profileImageClickListener?.invoke() + } + } + + override fun setSubtitle(subtitle: CharSequence?) { + if(subtitle.isNullOrEmpty()) { + setPadding(0, 0, 0, 0) + toolbarImage?.translationY = 0f + } else { + setPadding(0, -1, 0, 5) + toolbarImage?.translationY = 6f + } + super.setSubtitle(subtitle) + } + + private fun create(attrs: AttributeSet?, defStyle: Int) { + + } + + var subtitleFormat: Int? = null + var subtitleFormatWithUnread: Int? = null + + var profileImageClickListener: (() -> Unit)? = null + + var profileImage + get() = toolbarImage?.drawable + set(value) { + toolbarImage?.setImageDrawable(value) + } +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/NavView.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/NavView.kt new file mode 100644 index 00000000..496597c3 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/NavView.kt @@ -0,0 +1,213 @@ +package pl.szczodrzynski.navlib + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.graphics.Point +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.children +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.floatingactionbutton.FloatingActionButton +import kotlinx.android.synthetic.main.nav_view.view.* +import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet +import pl.szczodrzynski.navlib.drawer.NavDrawer + + +class NavView : FrameLayout { + companion object { + const val SOURCE_OTHER = 0 + const val SOURCE_DRAWER = 1 + const val SOURCE_BOTTOM_SHEET = 1 + } + + private var contentView: LinearLayout? = null + + private lateinit var statusBarBackground: View + private lateinit var navigationBarBackground: View + private lateinit var mainView: LinearLayout + private lateinit var floatingActionButton: FloatingActionButton + private lateinit var extendedFloatingActionButton: ExtendedFloatingActionButton + + lateinit var drawer: NavDrawer + lateinit var toolbar: NavToolbar + lateinit var bottomBar: NavBottomBar + lateinit var bottomSheet: NavBottomSheet + val coordinator by lazy { + findViewById(R.id.nv_coordinator) + } + + var navigationLoader: NavigationLoader? = null + + constructor(context: Context) : super(context) { + create(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + create(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { + create(attrs, defStyle) + } + + private fun create(attrs: AttributeSet?, defStyle: Int) { + // Load attributes + val a = context.obtainStyledAttributes(attrs, R.styleable.NavView, defStyle, 0) + /*_exampleString = a.getString( + R.styleable.NavView_exampleString + )*/ + a.recycle() + + val layoutInflater = LayoutInflater.from(context) + layoutInflater.inflate(R.layout.nav_view, this) + + contentView = findViewById(R.id.nv_content) + + statusBarBackground = findViewById(R.id.nv_statusBarBackground) + navigationBarBackground = findViewById(R.id.nv_navigationBarBackground) + mainView = findViewById(R.id.nv_main) + floatingActionButton = findViewById(R.id.nv_floatingActionButton) + extendedFloatingActionButton = findViewById(R.id.nv_extendedFloatingActionButton) + + drawer = NavDrawer( + context, + findViewById(R.id.nv_drawerLayout), + findViewById(R.id.nv_drawerContainerLandscape), + findViewById(R.id.nv_miniDrawerContainerPortrait), + findViewById(R.id.nv_miniDrawerElevation) + ) + toolbar = findViewById(R.id.nv_toolbar) + bottomBar = findViewById(R.id.nv_bottomBar) + bottomSheet = findViewById(R.id.nv_bottomSheet) + + drawer.toolbar = toolbar + drawer.bottomBar = bottomBar + + toolbar.toolbarImage = findViewById(R.id.nv_toolbar_image) + + bottomBar.drawer = drawer + bottomBar.bottomSheet = bottomSheet + bottomBar.fabView = floatingActionButton + bottomBar.fabExtendedView = extendedFloatingActionButton + + ripple.isEnabled = false + ripple.children.forEach { it.isEnabled = false } + + //bottomSheetBehavior.peekHeight = displayHeight + } + + private fun convertDpToPixel(dp: Float): Float { + val resources = context.resources + val metrics = resources.displayMetrics + return dp * (metrics.densityDpi / 160f) + } + + fun gainAttentionOnBottomBar() { + var x = ripple.width.toFloat() + var y = ripple.height.toFloat() + x -= convertDpToPixel(56f) / 2 + y -= convertDpToPixel(56f) / 2 + ripple.performRipple(Point(x.toInt(), y.toInt())) + } + + fun configSystemBarsUtil(systemBarsUtil: SystemBarsUtil) { + this.systemBarsUtil = systemBarsUtil.apply { + this.statusBarBgView = statusBarBackground + this.navigationBarBgView = navigationBarBackground + this.statusBarDarkView = nv_statusBarDarker + //this.navigationBarDarkView = navigationBarBackground + this.insetsListener = nv_drawerLayout + this.marginBySystemBars = mainView + this.paddingByNavigationBar = bottomSheet.getContentView() + } + } + + + var enableBottomSheet = true + var enableBottomSheetDrag = true + + var bottomBarEnable = true + get() = bottomBar.enable + set(value) { + field = value + bottomBar.enable = value + setContentMargins() // TODO combine bottomBarEnable and bottomBar.enable + } + + /** + * Shows the toolbar and sets the contentView's margin to be + * below the toolbar. + */ + var showToolbar = true; set(value) { + toolbar.visibility = if (value) View.VISIBLE else View.GONE + field = value + setContentMargins() + } + + /** + * Set the FAB's on click listener + */ + fun setFabOnClickListener(onClickListener: OnClickListener?) { + bottomBar.setFabOnClickListener(onClickListener) + } + + internal var systemBarsUtil: SystemBarsUtil? = null + + private fun setContentMargins() { + val layoutParams = CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + val actionBarSize = 56 * context.resources.displayMetrics.density + layoutParams.topMargin = if (showToolbar) actionBarSize.toInt() else 0 + layoutParams.bottomMargin = if (bottomBarEnable) actionBarSize.toInt() else 0 + contentView?.layoutParams = layoutParams + } + + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + + Log.d( + "NavLib", + "CONFIGURATION CHANGED: ${newConfig?.screenWidthDp}x${newConfig?.screenHeightDp} "+if (newConfig?.orientation == ORIENTATION_PORTRAIT) "portrait" else "landscape" + ) + + systemBarsUtil?.commit() + + drawer.decideDrawerMode( + newConfig?.orientation ?: ORIENTATION_PORTRAIT, + newConfig?.screenWidthDp ?: 0, + newConfig?.screenHeightDp ?: 0 + ) + } + + fun onBackPressed(): Boolean { + if (drawer.isOpen && !drawer.fixedDrawerEnabled()) { + if (drawer.profileSelectionIsOpen) { + drawer.profileSelectionClose() + return true + } + drawer.close() + return true + } + if (bottomSheet.isOpen) { + bottomSheet.close() + return true + } + return false + } + + override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { + if (contentView == null) { + super.addView(child, index, params) + } + else { + contentView!!.addView(child, index, params) + } + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/NavigationLoader.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/NavigationLoader.kt new file mode 100644 index 00000000..09428d74 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/NavigationLoader.kt @@ -0,0 +1,5 @@ +package pl.szczodrzynski.navlib + +interface NavigationLoader { + fun load(itemId: Int, callerId: Int, source: Int, args: Map) +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/SystemBarsUtil.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/SystemBarsUtil.kt new file mode 100644 index 00000000..07d4501c --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/SystemBarsUtil.kt @@ -0,0 +1,376 @@ +package pl.szczodrzynski.navlib + +import android.app.Activity +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.content.res.Resources +import android.graphics.Color +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES +import android.util.Log +import android.view.View +import android.view.View.* +import android.view.Window +import android.view.WindowManager +import androidx.core.graphics.ColorUtils +import androidx.core.view.ViewCompat +import com.mikepenz.materialize.util.KeyboardUtil + + +class SystemBarsUtil(private val activity: Activity) { + companion object { + private const val COLOR_TRANSPARENT = Color.TRANSPARENT + /** + * A fallback color. + * Tells to apply a #22000000 overlay over the status/nav bar color. + * This has the same effect as [statusBarDarker]. + */ + const val COLOR_HALF_TRANSPARENT = -1 + /** + * Use ?colorPrimaryDark as a fallback or status bar color. + */ + const val COLOR_PRIMARY_DARK = -2 + /** + * A fallback color. + * Not recommended to use as [statusBarFallbackLight] because it will make status bar + * icons almost invisible. + */ + const val COLOR_DO_NOT_CHANGE = -3 + + private const val TARGET_MODE_NORMAL = 0 + private const val TARGET_MODE_LIGHT = 1 + private const val TARGET_MODE_GRADIENT = 2 + } + + val window: Window by lazy { + activity.window + } + val resources: Resources by lazy { + activity.resources + } + + /** + * A view which will have the padding added when the soft input keyboard appears. + */ + var paddingByKeyboard: View? = null + /** + * Whether the app should be fullscreen. + * + * This means it will display under the system bars + * and you should probably provide [statusBarBgView], + * [navigationBarBgView] and [marginBySystemBars]. + */ + var appFullscreen = false + + /** + * Define the color used to tint the status bar background. + * + * Valid values are [COLOR_PRIMARY_DARK] or a color integer. + * + * You cannot use neither [COLOR_HALF_TRANSPARENT] nor [COLOR_DO_NOT_CHANGE] here. + * See [statusBarDarker]. + */ + var statusBarColor = COLOR_PRIMARY_DARK + /** + * Whether the status bar should have a dark overlay (#22000000). + * + * Useful if the [statusBarColor] is set to a bright color and is the same as an action bar. + * Not useful if [statusBarColor] is [COLOR_PRIMARY_DARK]. + */ + var statusBarDarker = false + /** + * A fallback status bar color used on Android Lollipop + * when the [statusBarColor] combined with [statusBarDarker] is + * too bright not to blend with status bar icons (they cannot be + * set to dark). + * + * This will (most likely) not be used when [statusBarDarker] is true. + * + * Valid values are [COLOR_HALF_TRANSPARENT], [COLOR_PRIMARY_DARK], [COLOR_DO_NOT_CHANGE]. + */ + var statusBarFallbackLight = COLOR_HALF_TRANSPARENT + /** + * A fallback status bar color used on Android KitKat and older. + * On these systems there is a black-to-transparent gradient as + * the status bar background. + * + * Valid values are [COLOR_HALF_TRANSPARENT], [COLOR_PRIMARY_DARK], [COLOR_DO_NOT_CHANGE]. + */ + var statusBarFallbackGradient = COLOR_DO_NOT_CHANGE + + // TODO remove - test for huawei + var statusBarTranslucent = false + + /** + * If false, the nav bar is mostly translucent but not completely transparent. + */ + var navigationBarTransparent = true + + /** + * A background view to be resized in order to fit under the status bar. + */ + var statusBarBgView: View? = null + /** + * A background view to be resized in order to fit under the nav bar. + */ + var navigationBarBgView: View? = null + + /** + * A dark, half-transparent view to be resized in order to fit under the status bar. + */ + var statusBarDarkView: View? = null + /** + * A dark, half-transparent view to be resized in order to fit under the nav bar. + */ + var navigationBarDarkView: View? = null + + /** + * A view which will have the margin added not to overlap with the status/nav bar. + */ + var marginBySystemBars: View? = null + /** + * A view which will listen to the inset applying. + */ + var insetsListener: View? = null + /** + * A view which will have the padding added not to overlap with the nav bar. + * Useful for persistent bottom sheets. + * Requires [marginBySystemBars]. + */ + var paddingByNavigationBar: View? = null + + private var keyboardUtil: KeyboardUtil? = null + private var insetsApplied = false + + fun commit() { + Log.d("NavLib", "SystemBarsUtil applying") + insetsApplied = false + if (paddingByKeyboard != null) { + // thanks mikepenz for this life-saving class + keyboardUtil = KeyboardUtil(activity, paddingByKeyboard) + keyboardUtil?.enable() + } + + // get the correct target SB color + var targetStatusBarColor = statusBarColor + if (targetStatusBarColor == COLOR_PRIMARY_DARK) + targetStatusBarColor = getColorFromAttr(activity, R.attr.colorPrimaryDark) + + var targetStatusBarDarker = statusBarDarker + + // fallback if the SB color is too light for the icons to be visible + // applicable on Lollipop 5.0 and TouchWiz 4.1-4.3 + var targetStatusBarFallbackLight = statusBarFallbackLight + if (targetStatusBarFallbackLight == COLOR_PRIMARY_DARK) + targetStatusBarFallbackLight = getColorFromAttr(activity, R.attr.colorPrimaryDark) + + // fallback if there is a gradient under the status bar + // applicable on AOSP/similar 4.4 and Huawei EMUI Lollipop + // TODO check huawei 6.0+ for gradient bars, check huawei 4.4 + var targetStatusBarFallbackGradient = statusBarFallbackGradient + if (targetStatusBarFallbackGradient == COLOR_PRIMARY_DARK) + targetStatusBarFallbackGradient = getColorFromAttr(activity, R.attr.colorPrimaryDark) + + // determines the target mode that will be applied + var targetStatusBarMode = TARGET_MODE_NORMAL + + val targetStatusBarLight = ColorUtils.calculateLuminance(targetStatusBarColor) > 0.75 && !targetStatusBarDarker + + if (appFullscreen) { + window.decorView.systemUiVisibility = 0 + // API 19+ (KitKat 4.4+) - make the app fullscreen. + // On lower APIs this is useless because + // #1 the status/nav bar cannot be transparent (except Samsung TouchWiz) + // #2 tablets do not report status/nav bar height correctly + // #3 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the activity not resize when keyboard is open + // Samsung TouchWiz - app will go fullscreen. There is a problem though, see #3. + var targetAppFullscreen = false + if (SDK_INT >= VERSION_CODES.KITKAT) { + targetAppFullscreen = true + } + + + if (SDK_INT in VERSION_CODES.KITKAT until VERSION_CODES.LOLLIPOP) { + // API 19-20 (KitKat 4.4) - set gradient status bar + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + // take FallbackGradient color + targetStatusBarMode = TARGET_MODE_GRADIENT + // disable darker even if [statusBarDarker] == true BUT gradient fallback is not COLOR_HALF_TRANSPARENT + //targetStatusBarDarker = targetStatusBarDarker && targetStatusBarFallbackGradient == COLOR_HALF_TRANSPARENT + } + else if (SDK_INT >= VERSION_CODES.LOLLIPOP) { + // API 21+ (Lollipop 5.0+) - set transparent status bar + if (statusBarTranslucent) { + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + } + else { + window.statusBarColor = Color.TRANSPARENT + } + if (SDK_INT < VERSION_CODES.M && targetStatusBarLight) { + // take FallbackLight color + targetStatusBarMode = TARGET_MODE_LIGHT + } + } + if (SDK_INT >= VERSION_CODES.M && targetStatusBarLight) { + // API 23+ (Marshmallow 6.0+) - set the status bar icons to dark color if [statusBarLight] is true + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + // FOR SAMSUNG/SONY DEVICES (TouchWiz 4.1-4.3) + if (SDK_INT < VERSION_CODES.KITKAT) { + val libs = activity.packageManager.systemSharedLibraryNames + var reflect: String? = null + // TODO galaxy s3 - opening keyboard does not resize activity if fullscreen + if (libs != null) { + for (lib in libs) { + Log.d("SBU", lib) + if (lib == "touchwiz") + // SYSTEM_UI_FLAG_TRANSPARENT_BACKGROUND = 0x00001000 + reflect = "SYSTEM_UI_FLAG_TRANSPARENT_BACKGROUND" + else if (lib.startsWith("com.sonyericsson.navigationbar")) + reflect = "SYSTEM_UI_FLAG_TRANSPARENT" + } + if (reflect != null) { + try { + val field = View::class.java.getField(reflect) + var flag = 0 + if (field.type === Integer.TYPE) + flag = field.getInt(null) + if (flag != 0) { + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or flag + targetStatusBarMode = TARGET_MODE_LIGHT /* or TARGET_MODE_GRADIENT */ + targetAppFullscreen = true + } + } catch (e: Exception) { + } + } + } + } + // TODO huawei detection for 5.0+ + + targetStatusBarColor = when (targetStatusBarMode) { + TARGET_MODE_LIGHT -> when (targetStatusBarFallbackLight) { + COLOR_DO_NOT_CHANGE -> targetStatusBarColor + COLOR_HALF_TRANSPARENT -> { + targetStatusBarDarker = true + targetStatusBarColor + } + else -> targetStatusBarFallbackLight + } + TARGET_MODE_GRADIENT -> when (targetStatusBarFallbackGradient) { + COLOR_DO_NOT_CHANGE -> { + targetStatusBarDarker = false + targetStatusBarColor + } + COLOR_HALF_TRANSPARENT -> { + targetStatusBarDarker = true + targetStatusBarColor + } + else -> { + targetStatusBarDarker = false + targetStatusBarFallbackGradient + } + } + else -> targetStatusBarColor + } + + statusBarBgView?.setBackgroundColor(targetStatusBarColor) + statusBarDarkView?.visibility = if (targetStatusBarDarker) VISIBLE else GONE + + if (targetAppFullscreen) { + window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + } + + // TODO navigation bar options like status bar + // NAVIGATION BAR + if (SDK_INT >= VERSION_CODES.KITKAT && (SDK_INT < VERSION_CODES.LOLLIPOP || !navigationBarTransparent)) { + // API 19-20 (KitKat 4.4) - set gradient navigation bar + // API 21+ (Lollipop 5.0+) - set half-transparent navigation bar if [navigationBarTransparent] is false + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) + } + + if (SDK_INT >= VERSION_CODES.LOLLIPOP && navigationBarTransparent) { + // API 21+ (Lollipop 5.0+) - set fully transparent navigation bar if [navigationBarTransparent] is true + window.navigationBarColor = Color.TRANSPARENT + } + + // PADDING + if (insetsListener != null) { + if (SDK_INT >= VERSION_CODES.LOLLIPOP && false) { + ViewCompat.setOnApplyWindowInsetsListener(insetsListener!!) { _, insets -> + Log.d("NavLib", "Got insets left = ${insets.systemWindowInsetLeft}, top = ${insets.systemWindowInsetTop}, right = ${insets.systemWindowInsetRight}, bottom = ${insets.systemWindowInsetBottom}") + if (insetsApplied) + return@setOnApplyWindowInsetsListener insets.consumeSystemWindowInsets() + Log.d("NavLib", "Applied insets left = ${insets.systemWindowInsetLeft}, top = ${insets.systemWindowInsetTop}, right = ${insets.systemWindowInsetRight}, bottom = ${insets.systemWindowInsetBottom}") + insetsApplied = true + applyPadding( + insets.systemWindowInsetLeft, + insets.systemWindowInsetTop, + insets.systemWindowInsetRight, + insets.systemWindowInsetBottom + ) + insets.consumeSystemWindowInsets() + } + } + else { + var statusBarSize = 0 + val statusBarRes = resources.getIdentifier("status_bar_height", "dimen", "android") + if (statusBarRes > 0 && targetAppFullscreen) { + statusBarSize = resources.getDimensionPixelSize(statusBarRes) + } + + + var navigationBarSize = 0 + if (hasNavigationBar(activity) && targetAppFullscreen) { + val orientation = resources.configuration.orientation + + val navigationBarRes = when { + orientation == ORIENTATION_PORTRAIT -> + resources.getIdentifier("navigation_bar_height", "dimen", "android") + isTablet(activity) -> + resources.getIdentifier("navigation_bar_height_landscape", "dimen", "android") + else -> + resources.getIdentifier("navigation_bar_width", "dimen", "android") + } + + if (navigationBarRes > 0) { + navigationBarSize = resources.getDimensionPixelSize(navigationBarRes) + } + } + + applyPadding( + 0, + statusBarSize, + 0, + navigationBarSize + ) + } + } + } + else { + // app not fullscreen + // TODO statusBarColor & navigationBarColor if not fullscreen (it's possible) + } + } + + private fun applyPadding(left: Int, top: Int, right: Int, bottom: Int) { + marginBySystemBars?.setPadding(left, top, right, bottom) + + statusBarBgView?.layoutParams?.height = top + navigationBarBgView?.layoutParams?.height = bottom + + statusBarDarkView?.layoutParams?.height = top + navigationBarDarkView?.layoutParams?.height = bottom + + paddingByNavigationBar?.setPadding( + (8 * resources.displayMetrics.density).toInt(), + 0, + (8 * resources.displayMetrics.density).toInt(), + bottom + ) + } + + fun destroy() { + if (paddingByKeyboard != null) { + keyboardUtil?.disable() + } + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/Utils.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/Utils.kt new file mode 100644 index 00000000..fc324f77 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/Utils.kt @@ -0,0 +1,164 @@ +package pl.szczodrzynski.navlib + +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.DisplayMetrics +import android.util.TypedValue +import android.view.View +import android.view.WindowManager +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import com.google.android.material.elevation.ElevationOverlayProvider +import com.mikepenz.iconics.IconicsColor +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.colorInt + + +/*private val displayMetrics by lazy { + context.resources.displayMetrics +}*/ +/*private val configuration by lazy { context.resources.configuration } +private val displayWidth: Int by lazy { configuration.screenWidthDp } +private val displayHeight: Int by lazy { configuration.screenHeightDp }*/ + +fun getTopInset(context: Context, view: View): Float { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + (view.rootWindowInsets?.systemWindowInsetTop ?: 24) + } else { + 24 + } * context.resources.displayMetrics.density +} +fun getLeftInset(context: Context, view: View): Float { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + (view.rootWindowInsets?.systemWindowInsetLeft ?: 0) + } else { + 0 + } * context.resources.displayMetrics.density +} +fun getRightInset(context: Context, view: View): Float { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + (view.rootWindowInsets?.systemWindowInsetRight ?: 0) + } else { + 0 + } * context.resources.displayMetrics.density +} +fun getBottomInset(context: Context, view: View): Float { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + (view.rootWindowInsets?.systemWindowInsetBottom ?: 48) + } else { + 48 + } * context.resources.displayMetrics.density +} + +fun View.getActivity(): Activity { + return findViewById(android.R.id.content).context as Activity +} + +fun blendColors(background: Int, foreground: Int): Int { + val r1 = (background shr 16 and 0xff) + val g1 = (background shr 8 and 0xff) + val b1 = (background and 0xff) + + val r2 = (foreground shr 16 and 0xff) + val g2 = (foreground shr 8 and 0xff) + val b2 = (foreground and 0xff) + val a2 = (foreground shr 24 and 0xff) + //ColorUtils.compositeColors() + + val factor = a2.toFloat() / 255f + val red = (r1 * (1 - factor) + r2 * factor) + val green = (g1 * (1 - factor) + g2 * factor) + val blue = (b1 * (1 - factor) + b2 * factor) + + return (0xff000000 or (red.toLong() shl 16) or (green.toLong() shl 8) or (blue.toLong())).toInt() +} + +fun elevateSurface(context: Context, dp: Int): Int { + ElevationOverlayProvider(context).apply { + return compositeOverlay(themeSurfaceColor, dp * context.resources.displayMetrics.density) + } +} + +fun isTablet(c: Context): Boolean { + return (c.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE +} + +fun hasNavigationBar(context: Context): Boolean { + val id = context.resources.getIdentifier("config_showNavigationBar", "bool", "android") + var hasNavigationBar = id > 0 && context.resources.getBoolean(id) + + if (!hasNavigationBar && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + val d = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + + val realDisplayMetrics = DisplayMetrics() + d.getRealMetrics(realDisplayMetrics) + + val realHeight = realDisplayMetrics.heightPixels + val realWidth = realDisplayMetrics.widthPixels + + val displayMetrics = DisplayMetrics() + d.getMetrics(displayMetrics) + + val displayHeight = displayMetrics.heightPixels + val displayWidth = displayMetrics.widthPixels + + hasNavigationBar = realWidth - displayWidth > 0 || realHeight - displayHeight > 0 + } + + // Allow a system property to override this. Used by the emulator. + // See also hasNavigationBar(). + val navBarOverride = System.getProperty("qemu.hw.mainkeys") + if (navBarOverride == "1") + hasNavigationBar = true + else if (navBarOverride == "0") hasNavigationBar = false + + return hasNavigationBar +} + +fun IconicsDrawable.colorAttr(context: Context, @AttrRes attrRes: Int) { + colorInt = getColorFromAttr(context, attrRes) +} + +fun getColorFromAttr(context: Context, @AttrRes color: Int): Int { + val typedValue = TypedValue() + context.theme.resolveAttribute(color, typedValue, true) + return typedValue.data +} + +fun Context.getDrawableFromRes(@DrawableRes id: Int): Drawable { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + resources.getDrawable(id, theme) + } + else { + resources.getDrawable(id) + } +} + +@ColorInt +fun Context.getColorFromRes(@ColorRes id: Int): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + resources.getColor(id, theme) + } + else { + resources.getColor(id) + } +} + +fun crc16(buffer: String): Int { + /* Note the change here */ + var crc = 0x1D0F + for (j in buffer) { + crc = crc.ushr(8) or (crc shl 8) and 0xffff + crc = crc xor (j.toInt() and 0xff)//byte to int, trunc sign + crc = crc xor (crc and 0xff shr 4) + crc = crc xor (crc shl 12 and 0xffff) + crc = crc xor (crc and 0xFF shl 5 and 0xffff) + } + crc = crc and 0xffff + return crc +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/BottomSheetAdapter.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/BottomSheetAdapter.kt new file mode 100644 index 00000000..bd2171d8 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/BottomSheetAdapter.kt @@ -0,0 +1,38 @@ +package pl.szczodrzynski.navlib.bottomsheet + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.navlib.R +import pl.szczodrzynski.navlib.bottomsheet.items.IBottomSheetItem +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem + +class BottomSheetAdapter(val items: List>) : RecyclerView.Adapter() { + + private val viewHolderProvider = ViewHolderProvider() + + init { + viewHolderProvider.registerViewHolderFactory(1, R.layout.nav_bs_item_primary) { itemView -> + BottomSheetPrimaryItem.ViewHolder(itemView) + } + viewHolderProvider.registerViewHolderFactory(2, R.layout.nav_bs_item_separator) { itemView -> + BottomSheetSeparatorItem.ViewHolder(itemView) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return viewHolderProvider.provideViewHolder(viewGroup = parent, viewType = viewType) + } + + override fun getItemViewType(position: Int): Int { + return items[position].viewType + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + items[position].bindViewHolder(viewHolder = holder) + } +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/NavBottomSheet.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/NavBottomSheet.kt new file mode 100644 index 00000000..024985ce --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/NavBottomSheet.kt @@ -0,0 +1,438 @@ +package pl.szczodrzynski.navlib.bottomsheet + +import android.app.Activity +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.inputmethod.InputMethodManager +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.widget.NestedScrollView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.navlibfont.NavLibFont +import com.mikepenz.iconics.utils.paddingDp +import com.mikepenz.iconics.utils.sizeDp +import pl.szczodrzynski.navlib.* +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem +import pl.szczodrzynski.navlib.bottomsheet.items.IBottomSheetItem + + +class NavBottomSheet : CoordinatorLayout { + companion object { + const val TOGGLE_GROUP_SINGLE_SELECTION = 0 + const val TOGGLE_GROUP_MULTIPLE_SELECTION = 1 + const val TOGGLE_GROUP_SORTING_ORDER = 2 + + const val SORT_MODE_ASCENDING = 0 + const val SORT_MODE_DESCENDING = 1 + } + + constructor(context: Context) : super(context) { + create(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + create(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { + create(attrs, defStyle) + } + + private lateinit var scrimView: View + private lateinit var bottomSheet: NestedScrollView + private lateinit var content: LinearLayout + private lateinit var dragBar: View + private lateinit var textInputLayout: TextInputLayout + private lateinit var textInputEditText: TextInputEditText + private lateinit var toggleGroupContainer: LinearLayout + private lateinit var toggleGroup: MaterialButtonToggleGroup + private lateinit var toggleGroupTitleView: TextView + private lateinit var list: RecyclerView + + private lateinit var bottomSheetBehavior: BottomSheetBehavior + private var bottomSheetVisible = false + + private val items = ArrayList>() + private val adapter = BottomSheetAdapter(items) + + /** + * Enable the bottom sheet. + * This value is mostly relevant to the [pl.szczodrzynski.navlib.NavBottomBar]. + */ + var enable = true + set(value) { + field = value + if (!value && bottomSheetVisible) + close() + } + /** + * Whether the [pl.szczodrzynski.navlib.NavBottomBar] should open this BottomSheet + * when the user drags the bottom bar. + */ + var enableDragToOpen = true + + /** + * Control the scrim view visibility, shown when BottomSheet + * is expanded. + */ + var scrimViewEnabled = true + set(value) { + scrimView.visibility = if (value) View.INVISIBLE else View.GONE // INVISIBLE + field = value + } + /** + * Whether tapping the Scrim view should hide the BottomSheet. + */ + var scrimViewTapToClose = true + + + fun hideKeyboard() { + val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(rootView.windowToken, 0) + } + + + private fun create(attrs: AttributeSet?, defStyle: Int) { + val layoutInflater = LayoutInflater.from(context) + layoutInflater.inflate(R.layout.nav_bottom_sheet, this) + + scrimView = findViewById(R.id.bs_scrim) + bottomSheet = findViewById(R.id.bs_view) + content = findViewById(R.id.bs_content) + dragBar = findViewById(R.id.bs_dragBar) + textInputLayout = findViewById(R.id.bs_textInputLayout) + textInputEditText = findViewById(R.id.bs_textInputEditText) + toggleGroupContainer = findViewById(R.id.bs_toggleGroupContainer) + toggleGroup = findViewById(R.id.bs_toggleGroup) + toggleGroupTitleView = findViewById(R.id.bs_toggleGroupTitle) + list = findViewById(R.id.bs_list) + + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + + scrimView.setOnTouchListener { _, event -> + if (!scrimViewTapToClose) + return@setOnTouchListener true + if (event.action == MotionEvent.ACTION_UP && bottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + true + } + + bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(v: View, p1: Float) {} + override fun onStateChanged(v: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN && bottomSheetVisible) { + bottomSheetVisible = false + bottomSheet.scrollTo(0, 0) + if (scrimViewEnabled) + Anim.fadeOut(scrimView, 300, null) + // steal the focus from any EditTexts + dragBar.requestFocus() + hideKeyboard() + onCloseListener?.invoke() + } + else if (!bottomSheetVisible) { + bottomSheetVisible = true + if (scrimViewEnabled) + Anim.fadeIn(scrimView, 300, null) + } + } + }) + + content.background.colorFilter = PorterDuffColorFilter( + elevateSurface(context, dp = 8), + PorterDuff.Mode.SRC_ATOP + ) + + // steal the focus from any EditTexts + dragBar.requestFocus() + + list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + adapter = this@NavBottomSheet.adapter + } + + toggleGroup.addOnButtonCheckedListener(toggleGroupCheckedListener) + textInputEditText.addTextChangedListener(textInputWatcher) + } + + var onCloseListener: (() -> Unit)? = null + + /* _____ _ + |_ _| | + | | | |_ ___ _ __ ___ ___ + | | | __/ _ \ '_ ` _ \/ __| + _| |_| || __/ | | | | \__ \ + |_____|\__\___|_| |_| |_|__*/ + operator fun plusAssign(item: IBottomSheetItem<*>) { + appendItem(item) + } + fun appendItem(item: IBottomSheetItem<*>) { + items.add(item) + adapter.notifyDataSetChanged() + //adapter.notifyItemInserted(items.size - 1) + } + fun appendItems(vararg items: IBottomSheetItem<*>) { + this.items.addAll(items) + adapter.notifyDataSetChanged() + //adapter.notifyItemRangeInserted(this.items.size - items.size, items.size) + } + fun prependItem(item: IBottomSheetItem<*>) { + items.add(0, item) + adapter.notifyDataSetChanged() + //adapter.notifyItemInserted(0) + } + fun prependItems(vararg items: IBottomSheetItem<*>) { + this.items.addAll(0, items.toList()) + adapter.notifyDataSetChanged() + //adapter.notifyItemRangeInserted(0, items.size) + } + fun addItemAt(index: Int, item: IBottomSheetItem<*>) { + items.add(index, item) + adapter.notifyDataSetChanged() + //adapter.notifyItemInserted(index) + } + fun removeItemById(id: Int) { + items.filterNot { it.id == id } + } + fun removeItemAt(index: Int) { + items.removeAt(index) + adapter.notifyDataSetChanged() + //adapter.notifyItemRemoved(index) + } + fun removeAllItems() { + items.clear() + adapter.notifyDataSetChanged() + } + fun removeAllStatic() { + items.removeAll { !it.isContextual } + adapter.notifyDataSetChanged() + } + fun removeAllContextual() { + items.removeAll { it.isContextual } + adapter.notifyDataSetChanged() + } + fun removeSeparators() { + items.removeAll { it is BottomSheetSeparatorItem } + adapter.notifyDataSetChanged() + } + + fun getItemById(id: Int, run: (it: IBottomSheetItem<*>?) -> Unit) { + items.singleOrNull { it.id == id }.also { + run(it) + if (it != null) + adapter.notifyItemChanged(items.indexOf(it)) + } + } + fun getItemByIndex(index: Int, run: (it: IBottomSheetItem<*>?) -> Unit) { + items.getOrNull(index).also { + run(it) + if (it != null) + adapter.notifyItemChanged(index) + } + } + + + /* _______ _ + |__ __| | | + | | ___ __ _ __ _| | ___ __ _ _ __ ___ _ _ _ __ + | |/ _ \ / _` |/ _` | |/ _ \ / _` | '__/ _ \| | | | '_ \ + | | (_) | (_| | (_| | | __/ | (_| | | | (_) | |_| | |_) | + |_|\___/ \__, |\__, |_|\___| \__, |_| \___/ \__,_| .__/ + __/ | __/ | __/ | | | + |___/ |___/ |___/ |*/ + var toggleGroupEnabled + get() = toggleGroupContainer.visibility == View.VISIBLE + set(value) { toggleGroupContainer.visibility = if (value) View.VISIBLE else View.GONE } + var toggleGroupTitle + get() = toggleGroupTitleView.text.toString() + set(value) { toggleGroupTitleView.text = value } + var toggleGroupSelectionMode: Int = TOGGLE_GROUP_SORTING_ORDER + set(value) { + field = value + toggleGroup.isSingleSelection = value != TOGGLE_GROUP_MULTIPLE_SELECTION + } + + private fun toggleGroupGetIconicsDrawable(context: Context, icon: IIcon?): Drawable? { + if (icon == null) + return null + return IconicsDrawable(context, icon).apply { + sizeDp = 24 + } + } + + fun toggleGroupAddItem(id: Int, text: String, @DrawableRes icon: Int, defaultSortOrder: Int = SORT_MODE_ASCENDING) { + toggleGroupAddItem(id, text, context.getDrawableFromRes(icon), defaultSortOrder) + } + fun toggleGroupAddItem(id: Int, text: String, icon: IIcon, defaultSortOrder: Int = SORT_MODE_ASCENDING) { + toggleGroupAddItem(id, text, toggleGroupGetIconicsDrawable(context, icon), defaultSortOrder) + } + fun toggleGroupAddItem(id: Int, text: String, icon: Drawable?, defaultSortOrder: Int = SORT_MODE_ASCENDING) { + if (id < 0) + throw IllegalArgumentException("ID cannot be less than 0") + toggleGroup.addView(MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply { + this.id = id + 1 + this.tag = defaultSortOrder + this.text = text + this.icon = icon + }, WRAP_CONTENT, WRAP_CONTENT) + } + fun toggleGroupCheck(id: Int) { + toggleGroup.check(id) + } + fun toggleGroupRemoveItems() { + toggleGroup.removeAllViews() + } + + private val toggleGroupCheckedListener = MaterialButtonToggleGroup.OnButtonCheckedListener { group, checkedId, isChecked -> + if (group.checkedButtonId == View.NO_ID) { + group.check(checkedId) + return@OnButtonCheckedListener + } + /* TAG bit order + * bit 0 = default sorting mode + * bit 1 = is checked + * bit 2 = current sorting mode + */ + if (toggleGroupSelectionMode == TOGGLE_GROUP_SORTING_ORDER) { + val button = group.findViewById(checkedId) ?: return@OnButtonCheckedListener + var tag = button.tag as Int + var sortingMode: Int? = null + if (isChecked) { + sortingMode = if (tag and 0b010 == 1 shl 1) { + /* the view is checked and clicked once again */ + if (tag and 0b100 == SORT_MODE_ASCENDING shl 2) SORT_MODE_DESCENDING else SORT_MODE_ASCENDING + } else { + /* the view is first clicked so use the default sorting mode */ + if (tag and 0b001 == SORT_MODE_ASCENDING) SORT_MODE_ASCENDING else SORT_MODE_DESCENDING + } + tag = tag and 0b001 /* retain only default sorting mode */ + tag = tag or 0b010 /* set as checked */ + tag = tag or (sortingMode shl 2) /* set new sorting mode */ + } + else { + tag = tag and 0b001 /* retain only default sorting mode */ + } + button.tag = tag + button.icon = toggleGroupGetIconicsDrawable(context, when (sortingMode) { + SORT_MODE_ASCENDING -> NavLibFont.Icon.nav_sort_ascending + SORT_MODE_DESCENDING -> NavLibFont.Icon.nav_sort_descending + else -> null + }) + if (sortingMode != null) { + toggleGroupSortingOrderListener?.invoke(checkedId, sortingMode) + } + } + else if (toggleGroup.isSingleSelection && isChecked) { + toggleGroupSingleSelectionListener?.invoke(checkedId - 1) + } + else { + toggleGroupMultipleSelectionListener?.invoke(checkedId - 1, isChecked) + } + } + + var toggleGroupSingleSelectionListener: ((id: Int) -> Unit)? = null + var toggleGroupMultipleSelectionListener: ((id: Int, checked: Boolean) -> Unit)? = null + var toggleGroupSortingOrderListener: ((id: Int, sortMode: Int) -> Unit)? = null + + + /* _______ _ _ _ + |__ __| | | (_) | | + | | _____ _| |_ _ _ __ _ __ _ _| |_ + | |/ _ \ \/ / __| | | '_ \| '_ \| | | | __| + | | __/> <| |_ | | | | | |_) | |_| | |_ + |_|\___/_/\_\\__| |_|_| |_| .__/ \__,_|\__| + | | + |*/ + var textInputEnabled + get() = textInputLayout.visibility == View.VISIBLE + set(value) { textInputLayout.visibility = if (value) View.VISIBLE else View.GONE } + var textInputText + get() = textInputEditText.text.toString() + set(value) { textInputEditText.setText(value) } + var textInputHint + get() = textInputLayout.hint.toString() + set(value) { textInputLayout.hint = value } + var textInputHelperText + get() = textInputLayout.helperText.toString() + set(value) { textInputLayout.helperText = value } + var textInputError + get() = textInputLayout.error + set(value) { textInputLayout.error = value } + var textInputIcon: Any? + get() = textInputLayout.startIconDrawable + set(value) { + textInputLayout.startIconDrawable = when (value) { + is Drawable -> value + is IIcon -> IconicsDrawable(context).apply { + icon = value + sizeDp = 24 + // colorInt = Color.BLACK + } + is Int -> context.getDrawableFromRes(value) + else -> null + } + } + + private var textInputWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + textInputChangedListener?.onTextChanged(s?.toString() ?: "", start, before, count) + } + } + + interface OnTextInputChangedListener { + fun onTextChanged(s: String, start: Int, before: Int, count: Int) + } + var textInputChangedListener: OnTextInputChangedListener? = null + + + + fun dispatchBottomBarEvent(event: MotionEvent) { + val location = IntArray(2) + bottomSheet.getLocationOnScreen(location) + event.setLocation(event.rawX - location[0], event.rawY - location[1]) + bottomSheet.dispatchTouchEvent(event) + } + + fun setContentPadding(left: Int, top: Int, right: Int, bottom: Int) { + content.setPadding(left, top, right, bottom) + } + fun getContentView() = content + + var isOpen + get() = bottomSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN + set(value) { + bottomSheetBehavior.state = if (value) BottomSheetBehavior.STATE_EXPANDED else BottomSheetBehavior.STATE_HIDDEN + } + fun open() { isOpen = true } + fun close() { isOpen = false } + fun toggle() { + if (!enable) + return + isOpen = !isOpen + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/ViewHolderProvider.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/ViewHolderProvider.kt new file mode 100644 index 00000000..76ac8cb5 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/ViewHolderProvider.kt @@ -0,0 +1,21 @@ +package pl.szczodrzynski.navlib.bottomsheet + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import kotlin.reflect.KClass + +class ViewHolderProvider { + private val viewHolderFactories = hashMapOf RecyclerView.ViewHolder>>() + + fun provideViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val (layoutId: Int, f: (View) -> RecyclerView.ViewHolder) = viewHolderFactories[viewType]!! + val view = LayoutInflater.from(viewGroup.getContext()).inflate(layoutId, viewGroup, false) + return f(view) + } + + fun registerViewHolderFactory(viewType: Int, layoutId: Int, viewHolderFactory: (View) -> RecyclerView.ViewHolder) { + viewHolderFactories[viewType] = Pair(layoutId, viewHolderFactory) + } +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/BottomSheetPrimaryItem.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/BottomSheetPrimaryItem.kt new file mode 100644 index 00000000..3f7631cb --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/BottomSheetPrimaryItem.kt @@ -0,0 +1,124 @@ +package pl.szczodrzynski.navlib.bottomsheet.items + +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.utils.sizeDp +import pl.szczodrzynski.navlib.ImageHolder +import pl.szczodrzynski.navlib.R +import pl.szczodrzynski.navlib.colorAttr +import pl.szczodrzynski.navlib.getColorFromAttr + +data class BottomSheetPrimaryItem(override val isContextual: Boolean = true) : IBottomSheetItem { + + /*_ _ + | | | | + | | __ _ _ _ ___ _ _| |_ + | | / _` | | | |/ _ \| | | | __| + | |___| (_| | |_| | (_) | |_| | |_ + |______\__,_|\__, |\___/ \__,_|\__| + __/ | + |__*/ + override var id: Int = -1 + override val viewType: Int + get() = 1 + override val layoutId: Int + get() = R.layout.nav_bs_item_primary + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val root = itemView.findViewById(R.id.item_root) + val image = itemView.findViewById(R.id.item_icon) + val text = itemView.findViewById(R.id.item_text) + val description = itemView.findViewById(R.id.item_description) + } + + override fun bindViewHolder(viewHolder: ViewHolder) { + viewHolder.root.setOnClickListener(onClickListener) + + viewHolder.image.setImageDrawable(IconicsDrawable(viewHolder.text.context).apply { + icon = iconicsIcon + colorAttr(viewHolder.text.context, android.R.attr.textColorSecondary) + sizeDp = 24 + }) + + viewHolder.description.visibility = View.VISIBLE + when { + descriptionRes != null -> viewHolder.description.setText(descriptionRes!!) + description != null -> viewHolder.description.text = description + else -> viewHolder.description.visibility = View.GONE + } + + when { + titleRes != null -> viewHolder.text.setText(titleRes!!) + else -> viewHolder.text.text = title + } + viewHolder.text.setTextColor(getColorFromAttr(viewHolder.text.context, android.R.attr.textColorPrimary)) + } + + /*_____ _ + | __ \ | | + | | | | __ _| |_ __ _ + | | | |/ _` | __/ _` | + | |__| | (_| | || (_| | + |_____/ \__,_|\__\__,*/ + var title: CharSequence? = null + @StringRes + var titleRes: Int? = null + var description: CharSequence? = null + @StringRes + var descriptionRes: Int? = null + var icon: ImageHolder? = null + var iconicsIcon: IIcon? = null + var onClickListener: View.OnClickListener? = null + + fun withId(id: Int): BottomSheetPrimaryItem { + this.id = id + return this + } + + fun withTitle(title: CharSequence): BottomSheetPrimaryItem { + this.title = title + this.titleRes = null + return this + } + fun withTitle(@StringRes title: Int): BottomSheetPrimaryItem { + this.title = null + this.titleRes = title + return this + } + + fun withDescription(description: CharSequence): BottomSheetPrimaryItem { + this.description = description + this.descriptionRes = null + return this + } + fun withDescription(@StringRes description: Int): BottomSheetPrimaryItem { + this.description = null + this.descriptionRes = description + return this + } + + fun withIcon(icon: Drawable): BottomSheetPrimaryItem { + this.icon = ImageHolder(icon) + return this + } + fun withIcon(@DrawableRes icon: Int): BottomSheetPrimaryItem { + this.icon = ImageHolder(icon) + return this + } + fun withIcon(icon: IIcon): BottomSheetPrimaryItem { + this.iconicsIcon = icon + return this + } + + fun withOnClickListener(onClickListener: View.OnClickListener): BottomSheetPrimaryItem { + this.onClickListener = onClickListener + return this + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/BottomSheetSeparatorItem.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/BottomSheetSeparatorItem.kt new file mode 100644 index 00000000..cf5403e3 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/BottomSheetSeparatorItem.kt @@ -0,0 +1,28 @@ +package pl.szczodrzynski.navlib.bottomsheet.items + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.navlib.R + +data class BottomSheetSeparatorItem(override val isContextual: Boolean = true) : IBottomSheetItem { + + /*_ _ + | | | | + | | __ _ _ _ ___ _ _| |_ + | | / _` | | | |/ _ \| | | | __| + | |___| (_| | |_| | (_) | |_| | |_ + |______\__,_|\__, |\___/ \__,_|\__| + __/ | + |__*/ + override var id: Int = -1 + override val viewType: Int + get() = 2 + override val layoutId: Int + get() = R.layout.nav_bs_item_separator + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) + + override fun bindViewHolder(viewHolder: ViewHolder) { + + } +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/IBottomSheetItem.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/IBottomSheetItem.kt new file mode 100644 index 00000000..e55f337f --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/bottomsheet/items/IBottomSheetItem.kt @@ -0,0 +1,19 @@ +package pl.szczodrzynski.navlib.bottomsheet.items + +import android.view.View +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import kotlin.reflect.KClass + +interface IBottomSheetItem { + + val isContextual: Boolean + var id: Int + val viewType: Int + val layoutId: Int + + fun bindViewHolder(viewHolder: T) + fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) { + bindViewHolder(viewHolder as T) + } +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/IDrawerProfile.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/IDrawerProfile.kt new file mode 100644 index 00000000..30c15166 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/IDrawerProfile.kt @@ -0,0 +1,17 @@ +package pl.szczodrzynski.navlib.drawer + +import android.content.Context +import android.graphics.drawable.Drawable +import android.widget.ImageView +import pl.szczodrzynski.navlib.ImageHolder + +interface IDrawerProfile { + val id: Int + var name: String + var subname: String? + var image: String? + + fun getImageDrawable(context: Context): Drawable? + fun getImageHolder(context: Context): ImageHolder? + fun applyImageTo(imageView: ImageView) +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/IUnreadCounter.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/IUnreadCounter.kt new file mode 100644 index 00000000..ef213c2a --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/IUnreadCounter.kt @@ -0,0 +1,8 @@ +package pl.szczodrzynski.navlib.drawer + +interface IUnreadCounter { + var profileId: Int + var type: Int + var drawerItemId: Int? + var count: Int +} \ No newline at end of file diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/NavDrawer.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/NavDrawer.kt new file mode 100644 index 00000000..63c66766 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/NavDrawer.kt @@ -0,0 +1,730 @@ +package pl.szczodrzynski.navlib.drawer + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.LayerDrawable +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.customview.widget.ViewDragHelper +import androidx.drawerlayout.widget.DrawerLayout +import com.mikepenz.fastadapter.IAdapter +import com.mikepenz.itemanimators.AlphaCrossFadeAnimator +import com.mikepenz.materialdrawer.* +import com.mikepenz.materialdrawer.holder.BadgeStyle +import com.mikepenz.materialdrawer.holder.ColorHolder +import com.mikepenz.materialdrawer.holder.StringHolder +import com.mikepenz.materialdrawer.model.BaseDrawerItem +import com.mikepenz.materialdrawer.model.MiniProfileDrawerItem +import com.mikepenz.materialdrawer.model.ProfileDrawerItem +import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem +import com.mikepenz.materialdrawer.model.interfaces.* +import com.mikepenz.materialdrawer.util.* +import com.mikepenz.materialdrawer.widget.AccountHeaderView +import com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView +import com.mikepenz.materialdrawer.widget.MiniDrawerSliderView +import com.mikepenz.materialize.util.UIUtils +import pl.szczodrzynski.navlib.* +import pl.szczodrzynski.navlib.R +import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem + +class NavDrawer( + val context: Context, + val drawerLayout: DrawerLayout, + val drawerContainerLandscape: FrameLayout, + val drawerContainerPortrait: FrameLayout, + val miniDrawerElevation: View +) { + companion object { + private const val DRAWER_MODE_NORMAL = 0 + private const val DRAWER_MODE_MINI = 1 + private const val DRAWER_MODE_FIXED = 2 + } + + private lateinit var activity: Activity + private val resources: Resources + get() = context.resources + + internal lateinit var toolbar: NavToolbar + internal lateinit var bottomBar: NavBottomBar + + private lateinit var drawer: MaterialDrawerSliderView + private lateinit var accountHeader: AccountHeaderView + private lateinit var miniDrawer: MiniDrawerSliderView + + private var drawerMode: Int = DRAWER_MODE_NORMAL + private var selection: Int = -1 + + lateinit var badgeStyle: BadgeStyle + + @SuppressLint("ClickableViewAccessibility") + fun init(activity: Activity) { + this.activity = activity + + /*badgeStyle = BadgeStyle( + R.drawable.material_drawer_badge, + getColorFromAttr(context, R.attr.colorError), + getColorFromAttr(context, R.attr.colorError), + getColorFromAttr(context, R.attr.colorOnError) + )*/ + + badgeStyle = BadgeStyle().apply { + textColor = ColorHolder.fromColor(Color.WHITE) + color = ColorHolder.fromColor(0xffd32f2f.toInt()) + } + + drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener { + override fun onDrawerStateChanged(newState: Int) {} + override fun onDrawerSlide(drawerView: View, slideOffset: Float) {} + override fun onDrawerClosed(drawerView: View) { + drawerClosedListener?.invoke() + profileSelectionClose() + } + override fun onDrawerOpened(drawerView: View) { + drawerOpenedListener?.invoke() + } + }) + + accountHeader = AccountHeaderView(context).apply { + headerBackground = ImageHolder(R.drawable.header) + displayBadgesOnSmallProfileImages = true + + onAccountHeaderListener = { view, profile, current -> + if (profile is ProfileSettingDrawerItem) { + drawerProfileSettingClickListener?.invoke(profile.identifier.toInt(), view) ?: false + } + else { + updateBadges() + if (current) { + close() + profileSelectionClose() + true + } + else { + (drawerProfileSelectedListener?.invoke(profile.identifier.toInt(), profile, current, view) ?: false).also { + setToolbarProfileImage(profileList.singleOrNull { it.id == profile.identifier.toInt() }) + } + } + } + } + + onAccountHeaderItemLongClickListener = { view, profile, current -> + if (profile is ProfileSettingDrawerItem) { + drawerProfileSettingLongClickListener?.invoke(profile.identifier.toInt(), view) ?: true + } + else { + drawerProfileLongClickListener?.invoke(profile.identifier.toInt(), profile, current, view) ?: false + } + } + + onAccountHeaderProfileImageListener = { view, profile, current -> + drawerProfileImageClickListener?.invoke(profile.identifier.toInt(), profile, current, view) ?: false + } + //.withTextColor(ContextCompat.getColor(context, R.color.material_drawer_dark_primary_text)) + } + + drawer = MaterialDrawerSliderView(context).apply { + accountHeader = this@NavDrawer.accountHeader + itemAnimator = AlphaCrossFadeAnimator() + //hasStableIds = true + + onDrawerItemClickListener = { _, drawerItem, position -> + if (drawerItem.identifier.toInt() == selection) { + false + } + else { + val consumed = drawerItemSelectedListener?.invoke(drawerItem.identifier.toInt(), position, drawerItem) + if (consumed == false || !drawerItem.isSelectable) { + setSelection(selection, false) + consumed == false + } + else if (consumed == true) { + when (drawerItem) { + is DrawerPrimaryItem -> toolbar.title = drawerItem.appTitle ?: drawerItem.name?.getText(context) ?: "" + is BaseDrawerItem<*, *> -> toolbar.title = drawerItem.name?.getText(context) ?: "" + } + false + } + else { + false + } + } + } + + onDrawerItemLongClickListener = { _, drawerItem, position -> + drawerItemLongClickListener?.invoke(drawerItem.identifier.toInt(), position, drawerItem) ?: true + } + } + + miniDrawer = MiniDrawerSliderView(context).apply { + drawer = this@NavDrawer.drawer + includeSecondaryDrawerItems = false + try { + this::class.java.getDeclaredField("onMiniDrawerItemClickListener").let { + it.isAccessible = true + it.set(this, { v: View?, position: Int, item: IDrawerItem<*>, type: Int -> + if (item is MiniProfileDrawerItem) { + profileSelectionOpen() + open() + true + } else false + }) + } + } catch (_: Exception) { } + } + + updateMiniDrawer() + + toolbar.profileImageClickListener = { + profileSelectionOpen() + open() + } + + val configuration = context.resources.configuration + decideDrawerMode( + configuration.orientation, + configuration.screenWidthDp, + configuration.screenHeightDp + ) + } + + /* _____ _ + |_ _| | + | | | |_ ___ _ __ ___ ___ + | | | __/ _ \ '_ ` _ \/ __| + _| |_| || __/ | | | | \__ \ + |_____|\__\___|_| |_| |_|__*/ + operator fun plusAssign(item: IDrawerItem<*>) { + appendItem(item) + } + fun appendItem(item: IDrawerItem<*>) { + drawer.addItems(item) + updateMiniDrawer() + } + fun appendItems(vararg items: IDrawerItem<*>) { + drawer.addItems(*items) + updateMiniDrawer() + } + fun prependItem(item: IDrawerItem<*>) { + drawer.addItemAtPosition(0, item) + updateMiniDrawer() + } + fun prependItems(vararg items: IDrawerItem<*>) { + drawer.addItemsAtPosition(0, *items) + updateMiniDrawer() + } + fun addItemAt(index: Int, item: IDrawerItem<*>) { + drawer.addItemAtPosition(index, item) + updateMiniDrawer() + } + fun addItemsAt(index: Int, vararg items: IDrawerItem<*>) { + drawer.addItemsAtPosition(index, *items) + updateMiniDrawer() + } + fun removeItemById(id: Int) { + drawer.removeItems(id.toLong()) + updateMiniDrawer() + } + fun removeItemAt(index: Int) { + drawer.removeItemByPosition(index) + updateMiniDrawer() + } + fun removeAllItems() { + drawer.removeAllItems() + updateMiniDrawer() + } + + fun getItemById(id: Int, run: (it: IDrawerItem<*>?) -> Unit) { + drawer.getDrawerItem(id.toLong()).also { + run(it) + if (it != null) + drawer.updateItem(it) + updateMiniDrawer() + } + } + fun getItemByIndex(index: Int, run: (it: IDrawerItem<*>?) -> Unit) { + drawer.itemAdapter.itemList.get(index).also { + run(it) + if (it != null) + drawer.updateItem(it) + updateMiniDrawer() + } + } + + fun setItems(vararg items: IDrawerItem<*>) { + drawer.removeAllItems() + drawer.addItems(*items) + updateMiniDrawer() + } + + /* _____ _ _ _ _ _ + | __ \ (_) | | | | | | | | + | |__) | __ ___ ____ _| |_ ___ _ __ ___ ___| |_| |__ ___ __| |___ + | ___/ '__| \ \ / / _` | __/ _ \ | '_ ` _ \ / _ \ __| '_ \ / _ \ / _` / __| + | | | | | |\ V / (_| | || __/ | | | | | | __/ |_| | | | (_) | (_| \__ \ + |_| |_| |_| \_/ \__,_|\__\___| |_| |_| |_|\___|\__|_| |_|\___/ \__,_|__*/ + private fun drawerSetDragMargin(size: Float) { + try { + val mDrawerLayout = drawerLayout + val mDragger = mDrawerLayout::class.java.getDeclaredField( + "mLeftDragger" + ) + mDragger.isAccessible = true + val draggerObj = mDragger.get(mDrawerLayout) as ViewDragHelper? + draggerObj?.edgeSize = size.toInt() + + // update for SDK >= 29 (Android 10) + val useSystemInsets = mDrawerLayout::class.java.getDeclaredField( + "sEdgeSizeUsingSystemGestureInsets" + ) + useSystemInsets.isAccessible = true + useSystemInsets.set(null, false) + } + catch (e: Exception) { + e.printStackTrace() + Toast.makeText(context, "Oops, proguard works", Toast.LENGTH_SHORT).show() + } + } + + var miniDrawerVisiblePortrait: Boolean? = null + set(value) { + field = value + val configuration = context.resources.configuration + decideDrawerMode( + configuration.orientation, + configuration.screenWidthDp, + configuration.screenHeightDp + ) + } + var miniDrawerVisibleLandscape: Boolean? = null + set(value) { + field = value + val configuration = context.resources.configuration + decideDrawerMode( + configuration.orientation, + configuration.screenWidthDp, + configuration.screenHeightDp + ) + } + + internal fun decideDrawerMode(orientation: Int, widthDp: Int, heightDp: Int) { + val drawerLayoutParams = DrawerLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT).apply { + gravity = Gravity.START + } + val fixedLayoutParams = FrameLayout.LayoutParams(UIUtils.convertDpToPixel(300f, context).toInt(), MATCH_PARENT) + + Log.d("NavLib", "Deciding drawer mode:") + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + Log.d("NavLib", "- fixed container disabled") + + if (drawerContainerLandscape.childCount > 0) { + drawerContainerLandscape.removeAllViews() + } + Log.d("NavLib", "- mini drawer land disabled") + + if (drawerLayout.indexOfChild(drawer) == -1) { + drawerLayout.addView(drawer, drawerLayoutParams) + } + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + Log.d("NavLib", "- slider enabled") + + if ((widthDp >= 480 && miniDrawerVisiblePortrait != false) || miniDrawerVisiblePortrait == true) { + if (drawerContainerPortrait.indexOfChild(miniDrawer) == -1) + drawerContainerPortrait.addView(miniDrawer) + Log.d("NavLib", "- mini drawer port enabled") + drawerSetDragMargin(72 * resources.displayMetrics.density) + drawerMode = DRAWER_MODE_MINI + updateMiniDrawer() + } + else { + if (drawerContainerPortrait.childCount > 0) { + drawerContainerPortrait.removeAllViews() + } + Log.d("NavLib", "- mini drawer port disabled") + drawerSetDragMargin(20 * resources.displayMetrics.density) + drawerMode = DRAWER_MODE_NORMAL + } + } + else { + if (drawerContainerPortrait.childCount > 0) { + drawerContainerPortrait.removeAllViews() + } + Log.d("NavLib", "- mini drawer port disabled") + + if ((widthDp in 480 until 900 && miniDrawerVisibleLandscape != false) || miniDrawerVisibleLandscape == true) { + if (drawerContainerLandscape.indexOfChild(miniDrawer) == -1) + drawerContainerLandscape.addView(miniDrawer) + Log.d("NavLib", "- mini drawer land enabled") + drawerSetDragMargin(72 * resources.displayMetrics.density) + drawerMode = DRAWER_MODE_MINI + updateMiniDrawer() + } + else { + if (drawerContainerLandscape.childCount > 0) { + drawerContainerLandscape.removeAllViews() + } + Log.d("NavLib", "- mini drawer land disabled") + drawerSetDragMargin(20 * resources.displayMetrics.density) + drawerMode = DRAWER_MODE_NORMAL + } + if (widthDp >= 900) { + // screen is big enough to show fixed drawer + if (drawerLayout.indexOfChild(drawer) != -1) { + // remove from slider + drawerLayout.removeView(drawer) + } + // lock the slider + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + Log.d("NavLib", "- slider disabled") + // add to fixed container + if (drawerContainerLandscape.indexOfChild(drawer) == -1) + drawerContainerLandscape.addView(drawer, fixedLayoutParams) + drawer.visibility = View.VISIBLE + Log.d("NavLib", "- fixed container enabled") + drawerMode = DRAWER_MODE_FIXED + } + else { + // screen is too small for the fixed drawer + if (drawerContainerLandscape.indexOfChild(drawer) != -1) { + // remove from fixed container + drawerContainerLandscape.removeView(drawer) + } + Log.d("NavLib", "- fixed container disabled") + // unlock the slider + drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + if (drawerLayout.indexOfChild(drawer) == -1) { + // add to slider + drawerLayout.addView(drawer, drawerLayoutParams) + } + Log.d("NavLib", "- slider enabled") + } + } + + miniDrawerElevation.visibility = if (drawerMode == DRAWER_MODE_MINI || drawerMode == DRAWER_MODE_FIXED) View.VISIBLE else View.GONE + } + + private fun updateMiniDrawer() { + selection = drawer.selectedItemIdentifier.toInt() + //if (drawerMode == DRAWER_MODE_MINI) + miniDrawer.createItems() + } + + /* _____ _ _ _ _ _ _ + | __ \ | | | (_) | | | | | | + | |__) | _| |__ | |_ ___ _ __ ___ ___| |_| |__ ___ __| |___ + | ___/ | | | '_ \| | |/ __| | '_ ` _ \ / _ \ __| '_ \ / _ \ / _` / __| + | | | |_| | |_) | | | (__ | | | | | | __/ |_| | | | (_) | (_| \__ \ + |_| \__,_|_.__/|_|_|\___| |_| |_| |_|\___|\__|_| |_|\___/ \__,_|__*/ + var isOpen + get() = drawerLayout.isOpen || drawerMode == DRAWER_MODE_FIXED + set(value) { + if (drawerMode == DRAWER_MODE_FIXED) + return + if (value && !isOpen) drawerLayout.open() else if (!value && isOpen) drawerLayout.close() + } + fun open() { isOpen = true } + fun close() { isOpen = false } + fun toggle() { isOpen = !isOpen } + + var profileSelectionIsOpen + get() = accountHeader.selectionListShown + set(value) { + if (value != profileSelectionIsOpen) + profileSelectionToggle() + } + fun profileSelectionOpen() { profileSelectionIsOpen = true } + fun profileSelectionClose() { profileSelectionIsOpen = false } + fun profileSelectionToggle() { accountHeader.selectionListShown = !accountHeader.selectionListShown } + + var drawerOpenedListener: (() -> Unit)? = null + var drawerClosedListener: (() -> Unit)? = null + var drawerItemSelectedListener: ((id: Int, position: Int, drawerItem: IDrawerItem<*>) -> Boolean)? = null + var drawerItemLongClickListener: ((id: Int, position: Int, drawerItem: IDrawerItem<*>) -> Boolean)? = null + var drawerProfileSelectedListener: ((id: Int, profile: IProfile, current: Boolean, view: View?) -> Boolean)? = null + var drawerProfileLongClickListener: ((id: Int, profile: IProfile, current: Boolean, view: View?) -> Boolean)? = null + var drawerProfileImageClickListener: ((id: Int, profile: IProfile, current: Boolean, view: View) -> Boolean)? = null + var drawerProfileImageLongClickListener: ((id: Int, profile: IProfile, current: Boolean, view: View) -> Boolean)? = null + var drawerProfileListEmptyListener: (() -> Unit)? = null + var drawerProfileSettingClickListener: ((id: Int, view: View?) -> Boolean)? = null + var drawerProfileSettingLongClickListener: ((id: Int, view: View?) -> Boolean)? = null + + fun miniDrawerEnabled(): Boolean = drawerMode == DRAWER_MODE_MINI + fun fixedDrawerEnabled(): Boolean = drawerMode == DRAWER_MODE_FIXED + + fun setSelection(id: Int, fireOnClick: Boolean = true) { + Log.d("NavDebug", "setSelection(id = $id, fireOnClick = $fireOnClick)") + // seems that this cannot be open, because the itemAdapter has Profile items + // instead of normal Drawer items... + profileSelectionClose() + selection = id + + if (drawer.selectedItemIdentifier != id.toLong()) { + + } + + if (drawer.selectedItemIdentifier != id.toLong() || !fireOnClick) + drawer.setSelectionAtPosition(drawer.getPosition(id.toLong()), fireOnClick) + + miniDrawer.setSelection(-1L) + if (drawerMode == DRAWER_MODE_MINI) + miniDrawer.setSelection(id.toLong()) + } + fun getSelection(): Int = selection + + // TODO 2019-08-27 add methods for Drawable, @DrawableRes + fun setAccountHeaderBackground(path: String?) { + if (path == null) { + accountHeader.headerBackground = ImageHolder(R.drawable.header) + return + } + accountHeader.headerBackground = ImageHolder(path) + } + + /* _____ __ _ _ + | __ \ / _(_) | + | |__) | __ ___ | |_ _| | ___ ___ + | ___/ '__/ _ \| _| | |/ _ \/ __| + | | | | | (_) | | | | | __/\__ \ + |_| |_| \___/|_| |_|_|\___||__*/ + private var profileList: MutableList = mutableListOf() + + fun addProfileSettings(vararg items: ProfileSettingDrawerItem) { + accountHeader.profiles?.addAll(items) + } + + private fun updateProfileList() { + // remove all profile items + val profiles = accountHeader.profiles?.filterNot { it is ProfileDrawerItem } as MutableList? + + if (profileList.isEmpty()) + drawerProfileListEmptyListener?.invoke() + + profileList.forEachIndexed { index, profile -> + val image = profile.getImageHolder(context) + ProfileDrawerItem() + .withIdentifier(profile.id.toLong()) + .withName(profile.name) + .withEmail(profile.subname) + .also { it.icon = image } + .withBadgeStyle(badgeStyle) + .withNameShown(true) + .also { profiles?.add(index, it) } + } + + accountHeader.profiles = profiles + + updateBadges() + updateMiniDrawer() + } + + fun setProfileList(profiles: MutableList) { + profileList = profiles as MutableList + updateProfileList() + } + private var currentProfileObj: IDrawerProfile? = null + val profileListEmpty: Boolean + get() = profileList.isEmpty() + var currentProfile: Int + get() = accountHeader.activeProfile?.identifier?.toInt() ?: -1 + set(value) { + Log.d("NavDebug", "currentProfile = $value") + accountHeader.setActiveProfile(value.toLong(), false) + currentProfileObj = profileList.singleOrNull { it.id == value } + setToolbarProfileImage(currentProfileObj) + updateBadges() + } + fun appendProfile(profile: IDrawerProfile) { + profileList.add(profile) + updateProfileList() + } + fun appendProfiles(vararg profiles: IDrawerProfile) { + profileList.addAll(profiles) + updateProfileList() + } + fun prependProfile(profile: IDrawerProfile) { + profileList.add(0, profile) + updateProfileList() + } + fun prependProfiles(vararg profiles: IDrawerProfile) { + profileList.addAll(0, profiles.asList()) + updateProfileList() + } + fun addProfileAt(index: Int, profile: IDrawerProfile) { + profileList.add(index, profile) + updateProfileList() + } + fun addProfilesAt(index: Int, vararg profiles: IDrawerProfile) { + profileList.addAll(index, profiles.asList()) + updateProfileList() + } + fun removeProfileById(id: Int) { + profileList = profileList.filterNot { it.id == id }.toMutableList() + updateProfileList() + } + fun removeProfileAt(index: Int) { + profileList.removeAt(index) + updateProfileList() + } + fun removeAllProfile() { + profileList.clear() + updateProfileList() + } + fun removeAllProfileSettings() { + accountHeader.profiles = accountHeader.profiles?.filterNot { it is ProfileSettingDrawerItem }?.toMutableList() + } + + fun getProfileById(id: Int, run: (it: IDrawerProfile?) -> Unit) { + profileList.singleOrNull { it.id == id }.also { + run(it) + updateProfileList() + } + } + fun getProfileByIndex(index: Int, run: (it: IDrawerProfile?) -> Unit) { + profileList.getOrNull(index).also { + run(it) + updateProfileList() + } + } + + private fun setToolbarProfileImage(profile: IDrawerProfile?) { + toolbar.profileImage = profile?.getImageDrawable(context) + } + + + /* ____ _ + | _ \ | | + | |_) | __ _ __| | __ _ ___ ___ + | _ < / _` |/ _` |/ _` |/ _ \/ __| + | |_) | (_| | (_| | (_| | __/\__ \ + |____/ \__,_|\__,_|\__, |\___||___/ + __/ | + |__*/ + private var unreadCounterList: MutableList = mutableListOf() + private val unreadCounterTypeMap = mutableMapOf() + + fun updateBadges() { + + currentProfileObj = profileList.singleOrNull { it.id == currentProfile } + + drawer.itemAdapter.itemList.items.forEachIndexed { index, item -> + if (item is Badgeable) { + item.badge = null + drawer.updateItem(item) + } + } + + var profileCounters = listOf() + + accountHeader.profiles?.forEach { profile -> + if (profile !is ProfileDrawerItem) return@forEach + val counters = unreadCounterList.filter { it.profileId == profile.identifier.toInt() } + val count = counters.sumBy { it.count } + val badge = when { + count == 0 -> null + count >= 99 -> StringHolder("99+") + else -> StringHolder(count.toString()) + } + if (profile.badge != badge) { + profile.badge = badge + accountHeader.updateProfile(profile) + } + + if (currentProfile == profile.identifier.toInt()) + profileCounters = counters + } + + Log.d("NavDebug", "updateBadges()") + profileCounters.map { + it.drawerItemId = unreadCounterTypeMap[it.type] + } + var totalCount = 0 + profileCounters.forEach { + if (it.drawerItemId == null) + return@forEach + if (it.profileId != currentProfile) { + //Log.d("NavDebug", "- Remove badge for ${it.drawerItemId}") + //drawer?.updateBadge(it.drawerItemId?.toLong() ?: 0, null) + return@forEach + } + Log.d("NavDebug", "- Set badge ${it.count} for ${it.drawerItemId}") + drawer.updateBadge( + it.drawerItemId?.toLong() ?: 0, + when { + it.count == 0 -> null + it.count >= 99 -> StringHolder("99+") + else -> StringHolder(it.count.toString()) + } + ) + totalCount += it.count + } + updateMiniDrawer() + + if (bottomBar.navigationIcon is LayerDrawable) { + (bottomBar.navigationIcon as LayerDrawable?)?.apply { + findDrawableByLayerId(R.id.ic_badge) + .takeIf { it is BadgeDrawable } + ?.also { badge -> + (badge as BadgeDrawable).setCount(totalCount.toString()) + mutate() + setDrawableByLayerId(R.id.ic_badge, badge) + } + } + } + + if (totalCount == 0) { + toolbar.subtitle = resources.getString( + toolbar.subtitleFormat ?: return, + currentProfileObj?.name ?: "" + ) + } + else { + toolbar.subtitle = resources.getQuantityString( + toolbar.subtitleFormatWithUnread ?: toolbar.subtitleFormat ?: return, + totalCount, + currentProfileObj?.name ?: "", + totalCount + ) + } + } + + fun setUnreadCounterList(unreadCounterList: MutableList) { + this.unreadCounterList = unreadCounterList as MutableList + updateBadges() + } + + fun addUnreadCounterType(type: Int, drawerItem: Int) { + unreadCounterTypeMap[type] = drawerItem + } + + data class UnreadCounter( + override var profileId: Int, + override var type: Int, + override var drawerItemId: Int?, + override var count: Int + ) : IUnreadCounter + + fun setUnreadCount(profileId: Int, type: Int, count: Int) { + val item = unreadCounterList.singleOrNull { + it.type == type && it.profileId == profileId + } + if (item != null) { + item.count = count + } + else { + unreadCounterList.add(UnreadCounter(profileId, type, null, count)) + } + updateBadges() + } +} diff --git a/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/items/DrawerPrimaryItem.kt b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/items/DrawerPrimaryItem.kt new file mode 100644 index 00000000..7a623747 --- /dev/null +++ b/navlib/src/main/java/pl/szczodrzynski/navlib/drawer/items/DrawerPrimaryItem.kt @@ -0,0 +1,18 @@ +package pl.szczodrzynski.navlib.drawer.items + +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem + +class DrawerPrimaryItem : PrimaryDrawerItem() { + var appTitle: String? = null + fun withAppTitle(appTitle: String?): PrimaryDrawerItem { + this.appTitle = appTitle + return this + } +} + +fun PrimaryDrawerItem.withAppTitle(appTitle: String?): PrimaryDrawerItem { + if (this !is DrawerPrimaryItem) + return this + this.appTitle = appTitle + return this +} \ No newline at end of file diff --git a/navlib/src/main/res/color/mtrl_filled_background_color.xml b/navlib/src/main/res/color/mtrl_filled_background_color.xml new file mode 100644 index 00000000..ed0fc82d --- /dev/null +++ b/navlib/src/main/res/color/mtrl_filled_background_color.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/navlib/src/main/res/color/text_input_layout_background.xml b/navlib/src/main/res/color/text_input_layout_background.xml new file mode 100644 index 00000000..7bfcd005 --- /dev/null +++ b/navlib/src/main/res/color/text_input_layout_background.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/navlib/src/main/res/drawable-v21/bs_item_background.xml b/navlib/src/main/res/drawable-v21/bs_item_background.xml new file mode 100644 index 00000000..1cf74bcc --- /dev/null +++ b/navlib/src/main/res/drawable-v21/bs_item_background.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/bottom_sheet_background.xml b/navlib/src/main/res/drawable/bottom_sheet_background.xml new file mode 100644 index 00000000..1e6a6852 --- /dev/null +++ b/navlib/src/main/res/drawable/bottom_sheet_background.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/bottom_sheet_control_bar.xml b/navlib/src/main/res/drawable/bottom_sheet_control_bar.xml new file mode 100644 index 00000000..ac5256a5 --- /dev/null +++ b/navlib/src/main/res/drawable/bottom_sheet_control_bar.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/bs_item_background.xml b/navlib/src/main/res/drawable/bs_item_background.xml new file mode 100644 index 00000000..59ad9b32 --- /dev/null +++ b/navlib/src/main/res/drawable/bs_item_background.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/bs_item_background_base.xml b/navlib/src/main/res/drawable/bs_item_background_base.xml new file mode 100644 index 00000000..454d22b1 --- /dev/null +++ b/navlib/src/main/res/drawable/bs_item_background_base.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/header.png b/navlib/src/main/res/drawable/header.png new file mode 100644 index 00000000..d0730a1a Binary files /dev/null and b/navlib/src/main/res/drawable/header.png differ diff --git a/navlib/src/main/res/drawable/ic_android.xml b/navlib/src/main/res/drawable/ic_android.xml new file mode 100644 index 00000000..fa2bc215 --- /dev/null +++ b/navlib/src/main/res/drawable/ic_android.xml @@ -0,0 +1,9 @@ + + + diff --git a/navlib/src/main/res/drawable/ic_light.xml b/navlib/src/main/res/drawable/ic_light.xml new file mode 100644 index 00000000..4f7c019c --- /dev/null +++ b/navlib/src/main/res/drawable/ic_light.xml @@ -0,0 +1,9 @@ + + + diff --git a/navlib/src/main/res/drawable/ic_menu_badge.xml b/navlib/src/main/res/drawable/ic_menu_badge.xml new file mode 100644 index 00000000..4355777f --- /dev/null +++ b/navlib/src/main/res/drawable/ic_menu_badge.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/ic_night.xml b/navlib/src/main/res/drawable/ic_night.xml new file mode 100644 index 00000000..008a4fbf --- /dev/null +++ b/navlib/src/main/res/drawable/ic_night.xml @@ -0,0 +1,9 @@ + + + diff --git a/navlib/src/main/res/drawable/placeholder.xml b/navlib/src/main/res/drawable/placeholder.xml new file mode 100644 index 00000000..0a9be837 --- /dev/null +++ b/navlib/src/main/res/drawable/placeholder.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/profile.xml b/navlib/src/main/res/drawable/profile.xml new file mode 100644 index 00000000..eea87f66 --- /dev/null +++ b/navlib/src/main/res/drawable/profile.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/navlib/src/main/res/drawable/shadow_bottom.xml b/navlib/src/main/res/drawable/shadow_bottom.xml new file mode 100644 index 00000000..d8dec9b0 --- /dev/null +++ b/navlib/src/main/res/drawable/shadow_bottom.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/navlib/src/main/res/drawable/shadow_right.xml b/navlib/src/main/res/drawable/shadow_right.xml new file mode 100644 index 00000000..36223703 --- /dev/null +++ b/navlib/src/main/res/drawable/shadow_right.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/navlib/src/main/res/layout/material_drawer.xml b/navlib/src/main/res/layout/material_drawer.xml new file mode 100644 index 00000000..9369ea24 --- /dev/null +++ b/navlib/src/main/res/layout/material_drawer.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/navlib/src/main/res/layout/material_drawer_slider.xml b/navlib/src/main/res/layout/material_drawer_slider.xml new file mode 100644 index 00000000..a903073d --- /dev/null +++ b/navlib/src/main/res/layout/material_drawer_slider.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/layout/nav_bottom_sheet.xml b/navlib/src/main/res/layout/nav_bottom_sheet.xml new file mode 100644 index 00000000..5cb25d10 --- /dev/null +++ b/navlib/src/main/res/layout/nav_bottom_sheet.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/navlib/src/main/res/layout/nav_bs_item_primary.xml b/navlib/src/main/res/layout/nav_bs_item_primary.xml new file mode 100644 index 00000000..7092d1a6 --- /dev/null +++ b/navlib/src/main/res/layout/nav_bs_item_primary.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/layout/nav_bs_item_separator.xml b/navlib/src/main/res/layout/nav_bs_item_separator.xml new file mode 100644 index 00000000..2f948ca4 --- /dev/null +++ b/navlib/src/main/res/layout/nav_bs_item_separator.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/layout/nav_view.xml b/navlib/src/main/res/layout/nav_view.xml new file mode 100644 index 00000000..4b46d117 --- /dev/null +++ b/navlib/src/main/res/layout/nav_view.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/navlib/src/main/res/values-w600dp/styles.xml b/navlib/src/main/res/values-w600dp/styles.xml new file mode 100644 index 00000000..6674487f --- /dev/null +++ b/navlib/src/main/res/values-w600dp/styles.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/navlib/src/main/res/values/attrs.xml b/navlib/src/main/res/values/attrs.xml new file mode 100644 index 00000000..a239e4e4 --- /dev/null +++ b/navlib/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/navlib/src/main/res/values/attrs_nav_view.xml b/navlib/src/main/res/values/attrs_nav_view.xml new file mode 100644 index 00000000..c6169965 --- /dev/null +++ b/navlib/src/main/res/values/attrs_nav_view.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/navlib/src/main/res/values/colors.xml b/navlib/src/main/res/values/colors.xml new file mode 100644 index 00000000..e881e7bd --- /dev/null +++ b/navlib/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + #202196f3 + #154FBC + + #ffffff + #242424 + #000000 + + #0dffffff + #12ffffff + #14ffffff + #17ffffff + #1cffffff + #1fffffff + #24ffffff + #26ffffff + #29ffffff + \ No newline at end of file diff --git a/navlib/src/main/res/values/dimens.xml b/navlib/src/main/res/values/dimens.xml new file mode 100644 index 00000000..3a8e4fd5 --- /dev/null +++ b/navlib/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 10sp + \ No newline at end of file diff --git a/navlib/src/main/res/values/strings.xml b/navlib/src/main/res/values/strings.xml new file mode 100644 index 00000000..6f67dcd6 --- /dev/null +++ b/navlib/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + NavLib + %1$s + diff --git a/navlib/src/main/res/values/styles.xml b/navlib/src/main/res/values/styles.xml new file mode 100644 index 00000000..6ad92c22 --- /dev/null +++ b/navlib/src/main/res/values/styles.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index 02529180..d8939d3d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name='Szkolny.eu' -include ':app' +include ':app', ':navlib', ':navlib-font'