diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3a3b4afd..e7e98741 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -119,7 +119,7 @@ dependencies { // implementation 'com.vladsch.flexmark:flexmark-all:0.64.8' // implementation("org.opencv:opencv-android:4.11.0") // build.gradle에 추가 - implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1") +// implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1") diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index 73a4fe29..8ed9daf7 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -524,6 +524,10 @@ open class LauncherActivity : CommonActivity() { var isKeyboardVisible = false + fun setAddr(str : String) { + binding.currentAddress.text = str + } + @SuppressLint("NewApi", "MissingPermission") override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() @@ -542,11 +546,6 @@ open class LauncherActivity : CommonActivity() { HeadsetActionButtonReceiver.register(this) - - binding.tabs.setOnCheckedChangeListener { g, id -> - showContents(id) - } - /* handle navigation back events */ handleBackPress() updateLocationService() @@ -563,6 +562,19 @@ open class LauncherActivity : CommonActivity() { if (intent?.action == Intent.ACTION_WEB_SEARCH) { openWithIntent(intent) } + + binding.share.setOnClickListener { + if (binding.currentAddress.text.length > 5) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + (binding.currentAddress.tag as? String)?.let{putExtra(Intent.EXTRA_TITLE, it)} + putExtra(Intent.EXTRA_TEXT, binding.currentAddress.text) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, "링크 공유하기") + startActivity(shareIntent) + } + } } fun showContents(id : Int) { @@ -609,7 +621,9 @@ open class LauncherActivity : CommonActivity() { } else -> {} } + binding.floatingActionMenu.close(false) } + private fun initGeckoRuntime() { if (sRuntime == null) { try { @@ -642,7 +656,7 @@ open class LauncherActivity : CommonActivity() { super.onDestroy() } -// var blutoothManager : BluetoothManager? = null + // var blutoothManager : BluetoothManager? = null override fun onStart() { super.onStart() // blutoothManager = BluetoothManager(this) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index f26606ce..08579389 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -27,6 +27,7 @@ import android.view.View import android.widget.CheckBox import android.widget.EditText import android.widget.ProgressBar +import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.net.toUri @@ -73,7 +74,7 @@ class GeckoWeb : BWebview { var decoViews = arrayListOf() override fun setVisibility(visibility: Int) { super.setVisibility(visibility) - decoViews.forEach { it.visibility = visibility } + decoViews.filter { it != null && it.id > -1 }.forEach { it.visibility = visibility } } interface OnSave { fun saved() @@ -438,12 +439,14 @@ class GeckoWeb : BWebview { } fun getFilterF() = String(java.util.Base64.getMimeDecoder().decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=".toByteArray())) + var currentTitle = "" val contentDelegate = object : GeckoSession.ContentDelegate { override fun onTitleChange( session: GeckoSession, title: String? ) { Blog.LOGE("onTitleChange $title") + currentTitle = title ?: "" super.onTitleChange(session, title) } @@ -620,11 +623,19 @@ class GeckoWeb : BWebview { } - decoViews?.forEach { - if (it.id == R.id.back) { - it.setOnClickListener { session.goBack() } + decoViews.filter { it != null && it.id > -1 }.forEach { + if (it != null && it.id > -1) { + if (it.id == R.id.back) { + it.setOnClickListener { session.goBack() } + } else if (it.id == R.id.current_address) { + (it as? TextView)?.let { + it.tag = currentTitle + it.text = url + } + } } } + } override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { @@ -888,12 +899,16 @@ class GeckoWeb : BWebview { } } else if (url.startsWith("http") == false) { nUrl = lastDomain - } else { + } else if(param != null && param?.length ?: 0 > 1) { nUrl = url.plus(param) + } else { + nUrl = url } + if (!privateMode && this.isVisible == false) { this.visibility = View.VISIBLE } + Blog.LOGE("nUrl >>>> ${nUrl}") diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt index 207ae239..0b4049db 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt @@ -34,7 +34,9 @@ import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.EditText +import android.widget.ImageButton import android.widget.ImageView +import android.widget.TextView import android.widget.Toast import androidx.annotation.NonNull import androidx.appcompat.app.AlertDialog @@ -44,7 +46,9 @@ import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import bums.lunatic.launcher.LauncherActivity import bums.lunatic.launcher.LauncherActivity.Companion.lActivity +import bums.lunatic.launcher.LunaticLauncher import bums.lunatic.launcher.R import bums.lunatic.launcher.common.letTrue import bums.lunatic.launcher.databinding.LauncherHomeBinding @@ -503,16 +507,11 @@ internal class RssHome : Fragment() { vote() } } - binding.share.setOnClickListener { - binding.geckoWeb.saveMd() - } + Blog.LOGE("useHiddenMenu >>> $openQuery") if (openQuery.length > 0) { binding.geckoWeb.loadUrl("https://www.google.com/search?q=${openQuery}") openQuery = "" - } else { - binding.privateBtn.visibility = View.GONE - binding.search.visibility = View.GONE } binding.geckoWeb?.mOnSave = object : GeckoWeb.OnSave{ override fun saved() { @@ -544,6 +543,7 @@ internal class RssHome : Fragment() { if (it.vote) { it.vote = false } + it.hide = true } } } @@ -607,11 +607,21 @@ internal class RssHome : Fragment() { } } val nullCursor = PointerIcon.getSystemIcon(requireContext(), PointerIcon.TYPE_NULL) + binding.search.setOnClickListener { searchKeyword() } + binding.search.setOnLongClickListener{ + ask() + true + } binding.root.setPointerIcon(nullCursor) binding.geckoWeb.decoViews.add(binding.hide) binding.geckoWeb.decoViews.add(binding.vote) binding.geckoWeb.decoViews.add(binding.progressBar) - binding.geckoWeb.decoViews.add(binding.back) + (activity as? LauncherActivity)?.let { activity -> + binding.geckoWeb.decoViews.add(activity.findViewById(R.id.current_address)) + binding.geckoWeb.decoViews.add(activity.findViewById(R.id.back)) + } + + return binding.root } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/SearchBottomSheet.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/SearchBottomSheet.kt index 419c649a..639ed12d 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/SearchBottomSheet.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/SearchBottomSheet.kt @@ -17,8 +17,10 @@ import android.widget.Button import android.widget.CheckBox import android.widget.EditText import android.widget.LinearLayout +import android.widget.RadioButton import android.widget.RadioGroup import android.widget.Toast +import androidx.core.view.forEach import bums.lunatic.launcher.R import bums.lunatic.launcher.model.RssDataType import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -62,6 +64,7 @@ class SearchBottomSheet : BottomSheetDialogFragment() { for (category in categories) { val btn = Button(requireContext()).apply { text = category.name + tag = category isAllCaps = false setBackgroundResource(android.R.drawable.btn_default) setTextColor(Color.WHITE) @@ -72,6 +75,17 @@ class SearchBottomSheet : BottomSheetDialogFragment() { this.isSelected = !this.isSelected triggerSearchWithDebounce(inputKeyword.text.toString()) } + setOnLongClickListener { + categoryContainer.forEach { + categoryStates[(it.tag as RssDataType).name] = false + it.isSelected = false + } + categoryStates[(it.tag as RssDataType).name] = true + it.isSelected = true + triggerSearchWithDebounce(inputKeyword.text.toString()) + true + } + } categoryContainer.addView(btn) } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/WebBottomSheet.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/WebBottomSheet.kt index 0f946350..7b07be0c 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/WebBottomSheet.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/WebBottomSheet.kt @@ -22,6 +22,7 @@ import android.widget.RadioButton import android.widget.RadioGroup import android.widget.Toast import androidx.core.view.children +import androidx.core.view.forEach import bums.lunatic.launcher.R import bums.lunatic.launcher.model.RssDataType import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -81,6 +82,12 @@ class WebBottomSheet : BottomSheetDialogFragment() { setOnClickListener { triggerSearchWithDebounce(inputKeyword.text.toString()) } +// setOnLongClickListener { +// categoryContainer.forEach { (it as? RadioButton)?.let { it.isChecked = false} } +// (it as? RadioButton)?.let { it.isChecked = } +// triggerSearchWithDebounce(inputKeyword.text.toString()) +// true +// } } categoryContainer.addView(btn) } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/model/CommunityData.kt b/app/src/main/kotlin/bums/lunatic/launcher/model/CommunityData.kt index 1e9ae77b..5564d118 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/model/CommunityData.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/model/CommunityData.kt @@ -249,6 +249,7 @@ class RssData : RealmObject, RssDataInterface { var chosung : String? = null var vote : Boolean = false var read : Int = 0 + var hide : Boolean = false @Ignore var mRssDataType : RssDataType? = null diff --git a/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt new file mode 100644 index 00000000..c3a5566f --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/utils/CommonUtils.kt @@ -0,0 +1,21 @@ +package bums.lunatic.launcher.utils + +import android.content.Context +import android.os.Build + + + +object CommonUtils { + fun dpToPx(context: Context, dp: Float): Int { + val scale: Float = context.getResources().getDisplayMetrics().density + return Math.round(dp * scale) + } + + fun hasJellyBean(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + } + + fun hasLollipop(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionButton.kt b/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionButton.kt new file mode 100644 index 00000000..1afe0b29 --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionButton.kt @@ -0,0 +1,1332 @@ +package bums.lunatic.launcher.view + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Outline +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.graphics.Xfermode +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.StateListDrawable +import android.graphics.drawable.shapes.OvalShape +import android.graphics.drawable.shapes.Shape +import android.os.Build +import android.os.Parcel +import android.os.Parcelable +import android.os.SystemClock +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.view.ViewOutlineProvider +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.widget.ImageButton +import android.widget.TextView +import bums.lunatic.launcher.R +import bums.lunatic.launcher.utils.CommonUtils +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min + +@SuppressLint("AppCompatCustomView") +class FloatingActionButton : ImageButton { + var mFabSize: Int = 0 + var mShowShadow: Boolean = false + var mShadowColor: Int = 0 + var mShadowRadius: Int = CommonUtils.dpToPx(getContext(), 4f) + var mShadowXOffset: Int = CommonUtils.dpToPx(getContext(), 1f) + var mShadowYOffset: Int = CommonUtils.dpToPx(getContext(), 3f) + + private var mColorNormal = 0 + private var mColorPressed = 0 + private var mColorDisabled = 0 + private var mColorRipple = 0 + private var mIcon: Drawable? = null + private val mIconSize: Int = CommonUtils.dpToPx(getContext(), 24f) + private var mShowAnimation: Animation? = null + private var mHideAnimation: Animation? = null + private var mLabelText: String? = null + private var mClickListener: OnClickListener? = null + private var mBackgroundDrawable: Drawable? = null + private var mUsingElevation = false + private var mUsingElevationCompat = false + + // Progress + private var mProgressBarEnabled = false + private var mProgressWidth: Int = CommonUtils.dpToPx(getContext(), 6f) + private var mProgressColor = 0 + private var mProgressBackgroundColor = 0 + private var mShouldUpdateButtonPosition = false + private var mOriginalX = -1f + private var mOriginalY = -1f + private var mButtonPositionSaved = false + private var mProgressCircleBounds = RectF() + private val mBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val mProgressPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private var mProgressIndeterminate = false + private var mLastTimeAnimated: Long = 0 + private var mSpinSpeed = 195.0f //The amount of degrees per second + private var mPausedTimeWithoutGrowing: Long = 0 + private var mTimeStartGrowing = 0.0 + private var mBarGrowingFromFront = true + private val mBarLength = 16 + private var mBarExtraLength = 0f + private var mCurrentProgress = 0f + private var mTargetProgress = 0f + private var mProgress = 0 + private var mAnimateProgress = false + private var mShouldProgressIndeterminate = false + private var mShouldSetProgress = false + + @get:Synchronized + @set:Synchronized + var max: Int = 100 + + @get:Synchronized + var isProgressBackgroundShown: Boolean = false + private set + + @JvmOverloads + constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super( + context, + attrs, + defStyleAttr + ) { + init(context, attrs, defStyleAttr) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + init(context, attrs, defStyleAttr) + } + + private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val attr = + context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, 0) + mColorNormal = attr.getColor(R.styleable.FloatingActionButton_fab_colorNormal, -0x25bcca) + mColorPressed = attr.getColor(R.styleable.FloatingActionButton_fab_colorPressed, -0x18afbd) + mColorDisabled = + attr.getColor(R.styleable.FloatingActionButton_fab_colorDisabled, -0x555556) + mColorRipple = attr.getColor(R.styleable.FloatingActionButton_fab_colorRipple, -0x66000001) + mShowShadow = attr.getBoolean(R.styleable.FloatingActionButton_fab_showShadow, true) + mShadowColor = attr.getColor(R.styleable.FloatingActionButton_fab_shadowColor, 0x66000000) + mShadowRadius = attr.getDimensionPixelSize( + R.styleable.FloatingActionButton_fab_shadowRadius, + mShadowRadius + ) + mShadowXOffset = attr.getDimensionPixelSize( + R.styleable.FloatingActionButton_fab_shadowXOffset, + mShadowXOffset + ) + mShadowYOffset = attr.getDimensionPixelSize( + R.styleable.FloatingActionButton_fab_shadowYOffset, + mShadowYOffset + ) + mFabSize = attr.getInt(R.styleable.FloatingActionButton_fab_size, SIZE_NORMAL) + mLabelText = attr.getString(R.styleable.FloatingActionButton_fab_label) + mShouldProgressIndeterminate = + attr.getBoolean(R.styleable.FloatingActionButton_fab_progress_indeterminate, false) + mProgressColor = + attr.getColor(R.styleable.FloatingActionButton_fab_progress_color, -0xff6978) + mProgressBackgroundColor = + attr.getColor(R.styleable.FloatingActionButton_fab_progress_backgroundColor, 0x4D000000) + this.max = attr.getInt(R.styleable.FloatingActionButton_fab_progress_max, this.max) + this.isProgressBackgroundShown = + attr.getBoolean(R.styleable.FloatingActionButton_fab_progress_showBackground, true) + + if (attr.hasValue(R.styleable.FloatingActionButton_fab_progress)) { + mProgress = attr.getInt(R.styleable.FloatingActionButton_fab_progress, 0) + mShouldSetProgress = true + } + + if (attr.hasValue(R.styleable.FloatingActionButton_fab_elevationCompat)) { + val elevation = attr.getDimensionPixelOffset( + R.styleable.FloatingActionButton_fab_elevationCompat, + 0 + ).toFloat() + if (isInEditMode()) { + setElevation(elevation) + } else { + setElevationCompat(elevation) + } + } + + initShowAnimation(attr) + initHideAnimation(attr) + attr.recycle() + + if (isInEditMode()) { + if (mShouldProgressIndeterminate) { + setIndeterminate(true) + } else if (mShouldSetProgress) { + saveButtonOriginalPosition() + setProgress(mProgress, false) + } + } + + // updateBackground(); + setClickable(true) + } + + private fun initShowAnimation(attr: TypedArray) { + val resourceId = attr.getResourceId( + R.styleable.FloatingActionButton_fab_showAnimation, + R.anim.fab_scale_up + ) + mShowAnimation = AnimationUtils.loadAnimation(getContext(), resourceId) + } + + private fun initHideAnimation(attr: TypedArray) { + val resourceId = attr.getResourceId( + R.styleable.FloatingActionButton_fab_hideAnimation, + R.anim.fab_scale_down + ) + mHideAnimation = AnimationUtils.loadAnimation(getContext(), resourceId) + } + + private val circleSize: Int + get() = getResources().getDimensionPixelSize( + if (mFabSize == SIZE_NORMAL) + R.dimen.fab_size_normal + else + R.dimen.fab_size_mini + ) + + private fun calculateMeasuredWidth(): Int { + var width = this.circleSize + calculateShadowWidth() + if (mProgressBarEnabled) { + width += mProgressWidth * 2 + } + return width + } + + private fun calculateMeasuredHeight(): Int { + var height = this.circleSize + calculateShadowHeight() + if (mProgressBarEnabled) { + height += mProgressWidth * 2 + } + return height + } + + fun calculateShadowWidth(): Int { + return if (hasShadow()) this.shadowX * 2 else 0 + } + + fun calculateShadowHeight(): Int { + return if (hasShadow()) this.shadowY * 2 else 0 + } + + private val shadowX: Int + get() = (mShadowRadius + abs(mShadowXOffset.toDouble())).toInt() + + private val shadowY: Int + get() = (mShadowRadius + abs(mShadowYOffset.toDouble())).toInt() + + private fun calculateCenterX(): Float { + return (getMeasuredWidth() / 2).toFloat() + } + + private fun calculateCenterY(): Float { + return (getMeasuredHeight() / 2).toFloat() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { +// super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(calculateMeasuredWidth(), calculateMeasuredHeight()) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (mProgressBarEnabled) { + if (this.isProgressBackgroundShown) { + canvas.drawArc(mProgressCircleBounds, 360f, 360f, false, mBackgroundPaint) + } + + var shouldInvalidate = false + + if (mProgressIndeterminate) { + shouldInvalidate = true + + val deltaTime = SystemClock.uptimeMillis() - mLastTimeAnimated + val deltaNormalized = deltaTime * mSpinSpeed / 1000.0f + + updateProgressLength(deltaTime) + + mCurrentProgress += deltaNormalized + if (mCurrentProgress > 360f) { + mCurrentProgress -= 360f + } + + mLastTimeAnimated = SystemClock.uptimeMillis() + var from = mCurrentProgress - 90 + var to = mBarLength + mBarExtraLength + + if (isInEditMode()) { + from = 0f + to = 135f + } + + canvas.drawArc(mProgressCircleBounds, from, to, false, mProgressPaint) + } else { + if (mCurrentProgress != mTargetProgress) { + shouldInvalidate = true + val deltaTime = + (SystemClock.uptimeMillis() - mLastTimeAnimated).toFloat() / 1000 + val deltaNormalized = deltaTime * mSpinSpeed + + if (mCurrentProgress > mTargetProgress) { + mCurrentProgress = max( + (mCurrentProgress - deltaNormalized).toDouble(), + mTargetProgress.toDouble() + ).toFloat() + } else { + mCurrentProgress = min( + (mCurrentProgress + deltaNormalized).toDouble(), + mTargetProgress.toDouble() + ).toFloat() + } + mLastTimeAnimated = SystemClock.uptimeMillis() + } + + canvas.drawArc(mProgressCircleBounds, -90f, mCurrentProgress, false, mProgressPaint) + } + + if (shouldInvalidate) { + invalidate() + } + } + } + + private fun updateProgressLength(deltaTimeInMillis: Long) { + if (mPausedTimeWithoutGrowing >= PAUSE_GROWING_TIME) { + mTimeStartGrowing += deltaTimeInMillis.toDouble() + + if (mTimeStartGrowing > BAR_SPIN_CYCLE_TIME) { + mTimeStartGrowing -= BAR_SPIN_CYCLE_TIME + mPausedTimeWithoutGrowing = 0 + mBarGrowingFromFront = !mBarGrowingFromFront + } + + val distance = + cos((mTimeStartGrowing / BAR_SPIN_CYCLE_TIME + 1) * Math.PI).toFloat() / 2 + 0.5f + val length = (BAR_MAX_LENGTH - mBarLength).toFloat() + + if (mBarGrowingFromFront) { + mBarExtraLength = distance * length + } else { + val newLength = length * (1 - distance) + mCurrentProgress += (mBarExtraLength - newLength) + mBarExtraLength = newLength + } + } else { + mPausedTimeWithoutGrowing += deltaTimeInMillis + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + saveButtonOriginalPosition() + + if (mShouldProgressIndeterminate) { + setIndeterminate(true) + mShouldProgressIndeterminate = false + } else if (mShouldSetProgress) { + setProgress(mProgress, mAnimateProgress) + mShouldSetProgress = false + } else if (mShouldUpdateButtonPosition) { + updateButtonPosition() + mShouldUpdateButtonPosition = false + } + super.onSizeChanged(w, h, oldw, oldh) + + setupProgressBounds() + setupProgressBarPaints() + updateBackground() + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + override fun setLayoutParams(params: ViewGroup.LayoutParams?) { + if (params is MarginLayoutParams && mUsingElevationCompat) { + params.leftMargin += this.shadowX + params.topMargin += this.shadowY + params.rightMargin += this.shadowX + params.bottomMargin += this.shadowY + } + super.setLayoutParams(params) + } + + fun updateBackground() { + val layerDrawable: LayerDrawable? + if (hasShadow()) { + layerDrawable = LayerDrawable( + arrayOf( + this.Shadow(), + createFillDrawable(), + this.iconDrawable + ) + ) + } else { + layerDrawable = LayerDrawable( + arrayOf( + createFillDrawable(), + this.iconDrawable + ) + ) + } + + var iconSize = -1 + if (this.iconDrawable != null) { + iconSize = max( + this.iconDrawable.getIntrinsicWidth().toDouble(), + this.iconDrawable.getIntrinsicHeight().toDouble() + ).toInt() + } + val iconOffset = (this.circleSize - (if (iconSize > 0) iconSize else mIconSize)) / 2 + var circleInsetHorizontal: Int = + (if (hasShadow()) mShadowRadius + abs(mShadowXOffset.toDouble()) else 0).toInt() + var circleInsetVertical: Int = + (if (hasShadow()) mShadowRadius + abs(mShadowYOffset.toDouble()) else 0).toInt() + + if (mProgressBarEnabled) { + circleInsetHorizontal += mProgressWidth + circleInsetVertical += mProgressWidth + } + + /*layerDrawable.setLayerInset( + mShowShadow ? 1 : 0, + circleInsetHorizontal, + circleInsetVertical, + circleInsetHorizontal, + circleInsetVertical + );*/ + layerDrawable.setLayerInset( + if (hasShadow()) 2 else 1, + circleInsetHorizontal + iconOffset, + circleInsetVertical + iconOffset, + circleInsetHorizontal + iconOffset, + circleInsetVertical + iconOffset + ) + + setBackgroundCompat(layerDrawable) + } + + protected val iconDrawable: Drawable + get() { + if (mIcon != null) { + return mIcon!! + } else { + return ColorDrawable(Color.TRANSPARENT) + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun createFillDrawable(): Drawable { + val drawable = StateListDrawable() + drawable.addState(intArrayOf(-android.R.attr.state_enabled), createCircleDrawable(mColorDisabled)) + drawable.addState(intArrayOf(android.R.attr.state_pressed), createCircleDrawable(mColorPressed)) + drawable.addState(intArrayOf(), createCircleDrawable(mColorNormal)) + + if (CommonUtils.hasLollipop()) { + val ripple = RippleDrawable( + ColorStateList( + arrayOf(intArrayOf()), + intArrayOf(mColorRipple) + ), drawable, null + ) + setOutlineProvider(object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()) + } + }) + setClipToOutline(true) + mBackgroundDrawable = ripple + return ripple + } + + mBackgroundDrawable = drawable + return drawable + } + + private fun createCircleDrawable(color: Int): Drawable { + val shapeDrawable = CircleDrawable(OvalShape()) + shapeDrawable.getPaint().setColor(color) + return shapeDrawable + } + + @Suppress("deprecation") + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private fun setBackgroundCompat(drawable: Drawable?) { + if (CommonUtils.hasJellyBean()) { + setBackground(drawable) + } else { + setBackgroundDrawable(drawable) + } + } + + private fun saveButtonOriginalPosition() { + if (!mButtonPositionSaved) { + if (mOriginalX == -1f) { + mOriginalX = getX() + } + + if (mOriginalY == -1f) { + mOriginalY = getY() + } + + mButtonPositionSaved = true + } + } + + private fun updateButtonPosition() { + val x: Float + val y: Float + if (mProgressBarEnabled) { + x = if (mOriginalX > getX()) getX() + mProgressWidth else getX() - mProgressWidth + y = if (mOriginalY > getY()) getY() + mProgressWidth else getY() - mProgressWidth + } else { + x = mOriginalX + y = mOriginalY + } + setX(x) + setY(y) + } + + private fun setupProgressBarPaints() { + mBackgroundPaint.setColor(mProgressBackgroundColor) + mBackgroundPaint.setStyle(Paint.Style.STROKE) + mBackgroundPaint.setStrokeWidth(mProgressWidth.toFloat()) + + mProgressPaint.setColor(mProgressColor) + mProgressPaint.setStyle(Paint.Style.STROKE) + mProgressPaint.setStrokeWidth(mProgressWidth.toFloat()) + } + + private fun setupProgressBounds() { + val circleInsetHorizontal = if (hasShadow()) this.shadowX else 0 + val circleInsetVertical = if (hasShadow()) this.shadowY else 0 + mProgressCircleBounds = RectF( + (circleInsetHorizontal + mProgressWidth / 2).toFloat(), + (circleInsetVertical + mProgressWidth / 2).toFloat(), + (calculateMeasuredWidth() - circleInsetHorizontal - mProgressWidth / 2).toFloat(), + (calculateMeasuredHeight() - circleInsetVertical - mProgressWidth / 2).toFloat() + ) + } + + var showAnimation: Animation + get() = mShowAnimation!! + set(showAnimation) { + mShowAnimation = showAnimation + } + + var hideAnimation: Animation + get() = mHideAnimation!! + set(hideAnimation) { + mHideAnimation = hideAnimation + } + + fun playShowAnimation() { + mHideAnimation!!.cancel() + startAnimation(mShowAnimation) + } + + fun playHideAnimation() { + mShowAnimation!!.cancel() + startAnimation(mHideAnimation) + } + + fun getOnClickListener(): OnClickListener? { + return mClickListener + } + + val labelView: Label? + get() = getTag(R.id.fab_label) as? Label + + fun setColors(colorNormal: Int, colorPressed: Int, colorRipple: Int) { + mColorNormal = colorNormal + mColorPressed = colorPressed + mColorRipple = colorRipple + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + fun onActionDown() { + if (mBackgroundDrawable is StateListDrawable) { + val drawable = mBackgroundDrawable as StateListDrawable + drawable.setState(intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed)) + } else if (CommonUtils.hasLollipop()) { + val ripple = mBackgroundDrawable as RippleDrawable + ripple.setState(intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed)) + ripple.setHotspot(calculateCenterX(), calculateCenterY()) + ripple.setVisible(true, true) + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + fun onActionUp() { + if (mBackgroundDrawable is StateListDrawable) { + val drawable = mBackgroundDrawable as StateListDrawable + drawable.setState(intArrayOf(android.R.attr.state_enabled)) + } else if (CommonUtils.hasLollipop()) { + val ripple = mBackgroundDrawable as RippleDrawable + ripple.setState(intArrayOf(android.R.attr.state_enabled)) + ripple.setHotspot(calculateCenterX(), calculateCenterY()) + ripple.setVisible(true, true) + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (mClickListener != null && isEnabled()) { + val label: Label? = getTag(R.id.fab_label) as Label? + if (label == null) return super.onTouchEvent(event) + + val action = event.getAction() + when (action) { + MotionEvent.ACTION_UP -> { + if (label != null) { + label.onActionUp() + } + onActionUp() + } + + MotionEvent.ACTION_CANCEL -> { + if (label != null) { + label.onActionUp() + } + onActionUp() + } + } + mGestureDetector.onTouchEvent(event) + } + return super.onTouchEvent(event) + } + + var mGestureDetector: GestureDetector = + GestureDetector(getContext(), object : SimpleOnGestureListener() { + override fun onDown(e: MotionEvent): Boolean { + val label: Label? = getTag(R.id.fab_label) as Label? + if (label != null) { + label.onActionDown() + } + onActionDown() + return super.onDown(e) + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + val label: Label? = getTag(R.id.fab_label) as Label? + if (label != null) { + label.onActionUp() + } + onActionUp() + return super.onSingleTapUp(e) + } + }) + + public override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + + val ss = ProgressSavedState(superState) + + ss.mCurrentProgress = this.mCurrentProgress + ss.mTargetProgress = this.mTargetProgress + ss.mSpinSpeed = this.mSpinSpeed + ss.mProgressWidth = this.mProgressWidth + ss.mProgressColor = this.mProgressColor + ss.mProgressBackgroundColor = this.mProgressBackgroundColor + ss.mShouldProgressIndeterminate = this.mProgressIndeterminate + ss.mShouldSetProgress = + this.mProgressBarEnabled && mProgress > 0 && !this.mProgressIndeterminate + ss.mProgress = this.mProgress + ss.mAnimateProgress = this.mAnimateProgress + ss.mShowProgressBackground = this.isProgressBackgroundShown + + return ss + } + + public override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is ProgressSavedState) { + super.onRestoreInstanceState(state) + return + } + + val ss = state + super.onRestoreInstanceState(ss.getSuperState()) + + this.mCurrentProgress = ss.mCurrentProgress + this.mTargetProgress = ss.mTargetProgress + this.mSpinSpeed = ss.mSpinSpeed + this.mProgressWidth = ss.mProgressWidth + this.mProgressColor = ss.mProgressColor + this.mProgressBackgroundColor = ss.mProgressBackgroundColor + this.mShouldProgressIndeterminate = ss.mShouldProgressIndeterminate + this.mShouldSetProgress = ss.mShouldSetProgress + this.mProgress = ss.mProgress + this.mAnimateProgress = ss.mAnimateProgress + this.isProgressBackgroundShown = ss.mShowProgressBackground + + this.mLastTimeAnimated = SystemClock.uptimeMillis() + } + + private inner class CircleDrawable : ShapeDrawable { + private var circleInsetHorizontal = 0 + private var circleInsetVertical = 0 + + private constructor() + + internal constructor(s: Shape?) : super(s) { + circleInsetHorizontal = + (if (hasShadow()) mShadowRadius + abs(mShadowXOffset.toDouble()) else 0).toInt() + circleInsetVertical = + (if (hasShadow()) mShadowRadius + abs(mShadowYOffset.toDouble()) else 0).toInt() + + if (mProgressBarEnabled) { + circleInsetHorizontal += mProgressWidth + circleInsetVertical += mProgressWidth + } + } + + override fun draw(canvas: Canvas) { + setBounds( + circleInsetHorizontal, circleInsetVertical, calculateMeasuredWidth() + - circleInsetHorizontal, calculateMeasuredHeight() - circleInsetVertical + ) + super.draw(canvas!!) + } + } + + private inner class Shadow : Drawable() { + private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val mErase = Paint(Paint.ANTI_ALIAS_FLAG) + private var mRadius = 0f + + init { + this.init() + } + + fun init() { + setLayerType(LAYER_TYPE_SOFTWARE, null) + mPaint.setStyle(Paint.Style.FILL) + mPaint.setColor(mColorNormal) + + mErase.setXfermode(PORTER_DUFF_CLEAR) + + if (!isInEditMode()) { + mPaint.setShadowLayer( + mShadowRadius.toFloat(), + mShadowXOffset.toFloat(), + mShadowYOffset.toFloat(), + mShadowColor + ) + } + + mRadius = (this@FloatingActionButton.circleSize / 2).toFloat() + + if (mProgressBarEnabled && this@FloatingActionButton.isProgressBackgroundShown) { + mRadius += mProgressWidth.toFloat() + } + } + + override fun draw(canvas: Canvas) { + canvas.drawCircle(calculateCenterX(), calculateCenterY(), mRadius, mPaint) + canvas.drawCircle(calculateCenterX(), calculateCenterY(), mRadius, mErase) + } + + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(cf: ColorFilter?) { + } + + @SuppressLint("WrongConstant") + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int { + return 0 + } + } + + internal class ProgressSavedState : BaseSavedState { + var mCurrentProgress: Float = 0f + var mTargetProgress: Float = 0f + var mSpinSpeed: Float = 0f + var mProgress: Int = 0 + var mProgressWidth: Int = 0 + var mProgressColor: Int = 0 + var mProgressBackgroundColor: Int = 0 + var mProgressBarEnabled: Boolean = false + var mProgressBarVisibilityChanged: Boolean = false + var mProgressIndeterminate: Boolean = false + var mShouldProgressIndeterminate: Boolean = false + var mShouldSetProgress: Boolean = false + var mAnimateProgress: Boolean = false + var mShowProgressBackground: Boolean = false + + constructor(superState: Parcelable?) : super(superState) + + private constructor(`in`: Parcel) : super(`in`) { + this.mCurrentProgress = `in`.readFloat() + this.mTargetProgress = `in`.readFloat() + this.mProgressBarEnabled = `in`.readInt() != 0 + this.mSpinSpeed = `in`.readFloat() + this.mProgress = `in`.readInt() + this.mProgressWidth = `in`.readInt() + this.mProgressColor = `in`.readInt() + this.mProgressBackgroundColor = `in`.readInt() + this.mProgressBarVisibilityChanged = `in`.readInt() != 0 + this.mProgressIndeterminate = `in`.readInt() != 0 + this.mShouldProgressIndeterminate = `in`.readInt() != 0 + this.mShouldSetProgress = `in`.readInt() != 0 + this.mAnimateProgress = `in`.readInt() != 0 + this.mShowProgressBackground = `in`.readInt() != 0 + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeFloat(this.mCurrentProgress) + out.writeFloat(this.mTargetProgress) + out.writeInt((if (mProgressBarEnabled) 1 else 0)) + out.writeFloat(this.mSpinSpeed) + out.writeInt(this.mProgress) + out.writeInt(this.mProgressWidth) + out.writeInt(this.mProgressColor) + out.writeInt(this.mProgressBackgroundColor) + out.writeInt(if (this.mProgressBarVisibilityChanged) 1 else 0) + out.writeInt(if (this.mProgressIndeterminate) 1 else 0) + out.writeInt(if (this.mShouldProgressIndeterminate) 1 else 0) + out.writeInt(if (this.mShouldSetProgress) 1 else 0) + out.writeInt(if (this.mAnimateProgress) 1 else 0) + out.writeInt(if (this.mShowProgressBackground) 1 else 0) + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = + object : Parcelable.Creator { + override fun createFromParcel(`in`: Parcel): ProgressSavedState { + return ProgressSavedState(`in`) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + } + + /* ===== API methods ===== */ + override fun setImageDrawable(drawable: Drawable?) { + if (mIcon !== drawable) { + mIcon = drawable + updateBackground() + } + } + + override fun setImageResource(resId: Int) { + val drawable = getResources().getDrawable(resId) + if (mIcon !== drawable) { + mIcon = drawable + updateBackground() + } + } + + override fun setOnClickListener(l: OnClickListener?) { + super.setOnClickListener(l) + mClickListener = l + val label = getTag(R.id.fab_label) as View? + if (label != null) { + label.setOnClickListener(object : OnClickListener { + override fun onClick(v: View?) { + if (mClickListener != null) { + mClickListener!!.onClick(this@FloatingActionButton) + } + } + }) + } + } + + var buttonSize: Int + get() = mFabSize + /** + * Sets the size of the **FloatingActionButton** and invalidates its layout. + * + * @param size size of the **FloatingActionButton**. Accepted values: SIZE_NORMAL, SIZE_MINI. + */ + set(size) { + require(!(size != SIZE_NORMAL && size != SIZE_MINI)) { "Use @FabSize constants only!" } + + if (mFabSize != size) { + mFabSize = size + updateBackground() + } + } + + fun setColorNormalResId(colorResId: Int) { + this.colorNormal = getResources().getColor(colorResId) + } + + var colorNormal: Int + get() = mColorNormal + set(color) { + if (mColorNormal != color) { + mColorNormal = color + updateBackground() + } + } + + fun setColorPressedResId(colorResId: Int) { + this.colorPressed = getResources().getColor(colorResId) + } + + var colorPressed: Int + get() = mColorPressed + set(color) { + if (color != mColorPressed) { + mColorPressed = color + updateBackground() + } + } + + fun setColorRippleResId(colorResId: Int) { + this.colorRipple = getResources().getColor(colorResId) + } + + var colorRipple: Int + get() = mColorRipple + set(color) { + if (color != mColorRipple) { + mColorRipple = color + updateBackground() + } + } + + fun setColorDisabledResId(colorResId: Int) { + this.colorDisabled = getResources().getColor(colorResId) + } + + var colorDisabled: Int + get() = mColorDisabled + set(color) { + if (color != mColorDisabled) { + mColorDisabled = color + updateBackground() + } + } + + fun setShowShadow(show: Boolean) { + if (mShowShadow != show) { + mShowShadow = show + updateBackground() + } + } + + fun hasShadow(): Boolean { + return !mUsingElevation && mShowShadow + } + + /** + * Sets the shadow radius of the **FloatingActionButton** and invalidates its layout. + * + * + * Must be specified in density-independent (dp) pixels, which are then converted into actual + * pixels (px). + * + * @param shadowRadiusDp shadow radius specified in density-independent (dp) pixels + */ + fun setShadowRadius(shadowRadiusDp: Float) { + mShadowRadius = CommonUtils.dpToPx(getContext(), shadowRadiusDp) + requestLayout() + updateBackground() + } + + var shadowRadius: Int + get() = mShadowRadius + /** + * Sets the shadow radius of the **FloatingActionButton** and invalidates its layout. + * + * @param dimenResId the resource identifier of the dimension + */ + set(dimenResId) { + val shadowRadius = getResources().getDimensionPixelSize(dimenResId) + if (mShadowRadius != shadowRadius) { + mShadowRadius = shadowRadius + requestLayout() + updateBackground() + } + } + + /** + * Sets the shadow x offset of the **FloatingActionButton** and invalidates its layout. + * + * + * Must be specified in density-independent (dp) pixels, which are then converted into actual + * pixels (px). + * + * @param shadowXOffsetDp shadow radius specified in density-independent (dp) pixels + */ + fun setShadowXOffset(shadowXOffsetDp: Float) { + mShadowXOffset = CommonUtils.dpToPx(getContext(), shadowXOffsetDp) + requestLayout() + updateBackground() + } + + var shadowXOffset: Int + get() = mShadowXOffset + /** + * Sets the shadow x offset of the **FloatingActionButton** and invalidates its layout. + * + * @param dimenResId the resource identifier of the dimension + */ + set(dimenResId) { + val shadowXOffset = getResources().getDimensionPixelSize(dimenResId) + if (mShadowXOffset != shadowXOffset) { + mShadowXOffset = shadowXOffset + requestLayout() + updateBackground() + } + } + + /** + * Sets the shadow y offset of the **FloatingActionButton** and invalidates its layout. + * + * + * Must be specified in density-independent (dp) pixels, which are then converted into actual + * pixels (px). + * + * @param shadowYOffsetDp shadow radius specified in density-independent (dp) pixels + */ + fun setShadowYOffset(shadowYOffsetDp: Float) { + mShadowYOffset = CommonUtils.dpToPx(getContext(), shadowYOffsetDp) + requestLayout() + updateBackground() + } + + var shadowYOffset: Int + get() = mShadowYOffset + /** + * Sets the shadow y offset of the **FloatingActionButton** and invalidates its layout. + * + * @param dimenResId the resource identifier of the dimension + */ + set(dimenResId) { + val shadowYOffset = getResources().getDimensionPixelSize(dimenResId) + if (mShadowYOffset != shadowYOffset) { + mShadowYOffset = shadowYOffset + requestLayout() + updateBackground() + } + } + + fun setShadowColorResource(colorResId: Int) { + val shadowColor = getResources().getColor(colorResId) + if (mShadowColor != shadowColor) { + mShadowColor = shadowColor + updateBackground() + } + } + + var shadowColor: Int + get() = mShadowColor + set(color) { + if (mShadowColor != color) { + mShadowColor = color + updateBackground() + } + } + + val isHidden: Boolean + /** + * Checks whether **FloatingActionButton** is hidden + * + * @return true if **FloatingActionButton** is hidden, false otherwise + */ + get() = getVisibility() == INVISIBLE + + /** + * Makes the **FloatingActionButton** to appear and sets its visibility to [.VISIBLE] + * + * @param animate if true - plays "show animation" + */ + fun show(animate: Boolean) { + if (this.isHidden) { + if (animate) { + playShowAnimation() + } + super.setVisibility(VISIBLE) + } + } + + /** + * Makes the **FloatingActionButton** to disappear and sets its visibility to [.INVISIBLE] + * + * @param animate if true - plays "hide animation" + */ + fun hide(animate: Boolean) { + if (!this.isHidden) { + if (animate) { + playHideAnimation() + } + super.setVisibility(INVISIBLE) + } + } + + fun toggle(animate: Boolean) { + if (this.isHidden) { + show(animate) + } else { + hide(animate) + } + } + + var labelText: String? + get() = mLabelText + set(text) { + mLabelText = text + val labelView: TextView? = this.labelView + if (labelView != null) { + labelView.setText(text) + } + } + + var labelVisibility: Int + get() { + val labelView: TextView? = this.labelView + if (labelView != null) { + return labelView.getVisibility() + } + + return -1 + } + set(visibility) { + val labelView: Label? = this.labelView + if (labelView != null) { + labelView.setVisibility(visibility) + labelView.isHandleVisibilityChanges = (visibility == VISIBLE) + } + } + + override fun setElevation(elevation: Float) { + if (CommonUtils.hasLollipop() && elevation > 0) { + super.setElevation(elevation) + if (!isInEditMode()) { + mUsingElevation = true + mShowShadow = false + } + updateBackground() + } + } + + /** + * Sets the shadow color and radius to mimic the native elevation. + * + * + * **API 21+**: Sets the native elevation of this view, in pixels. Updates margins to + * make the view hold its position in layout across different platform versions. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + fun setElevationCompat(elevation: Float) { + mShadowColor = 0x26000000 + mShadowRadius = Math.round(elevation / 2) + mShadowXOffset = 0 + mShadowYOffset = Math.round(if (mFabSize == SIZE_NORMAL) elevation else elevation / 2) + + if (CommonUtils.hasLollipop()) { + super.setElevation(elevation) + mUsingElevationCompat = true + mShowShadow = false + updateBackground() + + val layoutParams = getLayoutParams() + if (layoutParams != null) { + setLayoutParams(layoutParams) + } + } else { + mShowShadow = true + updateBackground() + } + } + + /** + * + * Change the indeterminate mode for the progress bar. In indeterminate + * mode, the progress is ignored and the progress bar shows an infinite + * animation instead. + * + * @param indeterminate true to enable the indeterminate mode + */ + @Synchronized + fun setIndeterminate(indeterminate: Boolean) { + if (!indeterminate) { + mCurrentProgress = 0.0f + } + + mProgressBarEnabled = indeterminate + mShouldUpdateButtonPosition = true + mProgressIndeterminate = indeterminate + mLastTimeAnimated = SystemClock.uptimeMillis() + setupProgressBounds() + // saveButtonOriginalPosition(); + updateBackground() + } + + @Synchronized + fun setProgress(progress: Int, animate: Boolean) { + var progress = progress + if (mProgressIndeterminate) return + + mProgress = progress + mAnimateProgress = animate + + if (!mButtonPositionSaved) { + mShouldSetProgress = true + return + } + + mProgressBarEnabled = true + mShouldUpdateButtonPosition = true + setupProgressBounds() + saveButtonOriginalPosition() + updateBackground() + + if (progress < 0) { + progress = 0 + } else if (progress > this.max) { + progress = this.max + } + + if (progress.toFloat() == mTargetProgress) { + return + } + + mTargetProgress = if (this.max > 0) (progress / max.toFloat()) * 360 else 0f + mLastTimeAnimated = SystemClock.uptimeMillis() + + if (!animate) { + mCurrentProgress = mTargetProgress + } + + invalidate() + } + + @get:Synchronized + val progress: Int + get() = if (mProgressIndeterminate) 0 else mProgress + + @Synchronized + fun hideProgress() { + mProgressBarEnabled = false + mShouldUpdateButtonPosition = true + updateBackground() + } + + @Synchronized + fun setShowProgressBackground(show: Boolean) { + this.isProgressBackgroundShown = show + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + val label: Label? = getTag(R.id.fab_label) as Label? + if (label != null) { + label.setEnabled(enabled) + } + } + + override fun setVisibility(visibility: Int) { + super.setVisibility(visibility) + val label: Label? = getTag(R.id.fab_label) as Label? + if (label != null) { + label.setVisibility(visibility) + } + } + + /** + * **This will clear all AnimationListeners.** + */ + fun hideButtonInMenu(animate: Boolean) { + if (!this.isHidden && getVisibility() != GONE) { + hide(animate) + + val label: Label? = this.labelView + if (label != null) { + label.hide(animate) + } + + this.hideAnimation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation?) { + } + + override fun onAnimationEnd(animation: Animation?) { + setVisibility(GONE) + this@FloatingActionButton.hideAnimation.setAnimationListener(null) + } + + override fun onAnimationRepeat(animation: Animation?) { + } + }) + } + } + + fun showButtonInMenu(animate: Boolean) { + if (getVisibility() == VISIBLE) return + + setVisibility(INVISIBLE) + show(animate) + val label: Label? = this.labelView + if (label != null) { + label.show(animate) + } + } + + /** + * Set the label's background colors + */ + fun setLabelColors(colorNormal: Int, colorPressed: Int, colorRipple: Int) { + val label: Label? = this.labelView + + val left: Int = label?.getPaddingLeft() ?: 0 + val top: Int = label?.getPaddingTop() ?: 0 + val right: Int = label?.getPaddingRight() ?: 0 + val bottom: Int = label?.getPaddingBottom() ?: 0 + + label?.setColors(colorNormal, colorPressed, colorRipple) + label?.updateBackground() + label?.setPadding(left, top, right, bottom) + } + + fun setLabelTextColor(color: Int) { + this.labelView?.setTextColor(color) + } + + fun setLabelTextColor(colors: ColorStateList?) { + this.labelView?.setTextColor(colors) + } + + companion object { + const val SIZE_NORMAL: Int = 0 + const val SIZE_MINI: Int = 1 + + private val PORTER_DUFF_CLEAR: Xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + private const val PAUSE_GROWING_TIME: Long = 200 + private const val BAR_SPIN_CYCLE_TIME = 500.0 + private const val BAR_MAX_LENGTH = 270 + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt b/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt new file mode 100644 index 00000000..b1c98653 --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/view/FloatingActionMenu.kt @@ -0,0 +1,1230 @@ +package bums.lunatic.launcher.view + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.TypedArray +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.os.Handler +import android.text.TextUtils +import android.util.AttributeSet +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.view.animation.AnticipateInterpolator +import android.view.animation.Interpolator +import android.view.animation.OvershootInterpolator +import android.widget.ImageView +import android.widget.ImageView.ScaleType +import bums.lunatic.launcher.R +import bums.lunatic.launcher.utils.CommonUtils + +import kotlin.math.abs +import kotlin.math.max + +class FloatingActionMenu @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ViewGroup(context, attrs, defStyleAttr) { + private val mOpenAnimatorSet = AnimatorSet() + private val mCloseAnimatorSet = AnimatorSet() + var iconToggleAnimatorSet: AnimatorSet? = null + + private var mButtonSpacing: Int = CommonUtils.dpToPx(getContext(), 0f) + private var mMenuButton: FloatingActionButton? = null + private var mMaxButtonWidth = 0 + private var mLabelsMargin: Int = CommonUtils.dpToPx(getContext(), 0f) + private val mLabelsVerticalOffset: Int = CommonUtils.dpToPx(getContext(), 0f) + private var mButtonsCount = 0 + + /* ===== API methods ===== */ + var isOpened: Boolean = false + private set + private var mIsMenuOpening = false + private val mUiHandler = Handler() + private var mLabelsShowAnimation = 0 + private var mLabelsHideAnimation = 0 + private var mLabelsPaddingTop: Int = CommonUtils.dpToPx(getContext(), 4f) + private var mLabelsPaddingRight: Int = CommonUtils.dpToPx(getContext(), 8f) + private var mLabelsPaddingBottom: Int = CommonUtils.dpToPx(getContext(), 4f) + private var mLabelsPaddingLeft: Int = CommonUtils.dpToPx(getContext(), 8f) + private var mLabelsTextColor: ColorStateList? = null + private var mLabelsTextSize = 0f + private var mLabelsCornerRadius: Int = CommonUtils.dpToPx(getContext(), 3f) + private var mLabelsShowShadow = false + private var mLabelsColorNormal = 0 + private var mLabelsColorPressed = 0 + private var mLabelsColorRipple = 0 + private var mMenuShowShadow = false + private var mMenuShadowColor = 0 + private var mMenuShadowRadius = 4f + private var mMenuShadowXOffset = 1f + private var mMenuShadowYOffset = 3f + private var mMenuColorNormal = 0 + private var mMenuColorPressed = 0 + private var mMenuColorRipple = 0 + private var mIcon: Drawable? = null + var animationDelayPerItem: Int = 0 + private var mOpenInterpolator: Interpolator? = null + private var mCloseInterpolator: Interpolator? = null + private var mIsAnimated = true + private var mLabelsSingleLine = false + private var mLabelsEllipsize = 0 + private var mLabelsMaxLines = 0 + private var mMenuFabSize = 0 + private var mLabelsStyle = 0 + private var mCustomTypefaceFromFont: Typeface? = null + var isIconAnimated: Boolean = true + private var mImageToggle: ImageView? = null + private var mMenuButtonShowAnimation: Animation? = null + private var mMenuButtonHideAnimation: Animation? = null + private var mImageToggleShowAnimation: Animation? = null + private var mImageToggleHideAnimation: Animation? = null + private var mIsMenuButtonAnimationRunning = false + private var mIsSetClosedOnTouchOutside = false + private var mOpenDirection = 0 + private var mToggleListener: OnMenuToggleListener? = null + + private var mShowBackgroundAnimator: ValueAnimator? = null + private var mHideBackgroundAnimator: ValueAnimator? = null + private var mBackgroundColor = 0 + + private var mLabelsPosition = 0 + private var mLabelsContext: Context? = null + private var mMenuLabelText: String? = null + private var mUsingMenuLabel = false + + private var isDragMenuDisabled = false + + interface OnMenuToggleListener { + fun onMenuToggle(opened: Boolean) + } + + init { + init(context, attrs) + } + + private fun init(context: Context, attrs: AttributeSet?) { + val attr = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionMenu, 0, 0) + mButtonSpacing = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_buttonSpacing, + mButtonSpacing + ) + mLabelsMargin = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_labels_margin, + mLabelsMargin + ) + mLabelsPosition = + attr.getInt(R.styleable.FloatingActionMenu_menu_labels_position, LABELS_POSITION_LEFT) + mLabelsShowAnimation = attr.getResourceId( + R.styleable.FloatingActionMenu_menu_labels_showAnimation, + if (mLabelsPosition == LABELS_POSITION_LEFT) R.anim.fab_slide_in_from_right else R.anim.fab_slide_in_from_left + ) + mLabelsHideAnimation = attr.getResourceId( + R.styleable.FloatingActionMenu_menu_labels_hideAnimation, + if (mLabelsPosition == LABELS_POSITION_LEFT) R.anim.fab_slide_out_to_right else R.anim.fab_slide_out_to_left + ) + mLabelsPaddingTop = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_labels_paddingTop, + mLabelsPaddingTop + ) + mLabelsPaddingRight = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_labels_paddingRight, + mLabelsPaddingRight + ) + mLabelsPaddingBottom = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_labels_paddingBottom, + mLabelsPaddingBottom + ) + mLabelsPaddingLeft = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_labels_paddingLeft, + mLabelsPaddingLeft + ) + mLabelsTextColor = + attr.getColorStateList(R.styleable.FloatingActionMenu_menu_labels_textColor) + // set default value if null same as for textview + if (mLabelsTextColor == null) { + mLabelsTextColor = ColorStateList.valueOf(Color.WHITE) + } + mLabelsTextSize = attr.getDimension( + R.styleable.FloatingActionMenu_menu_labels_textSize, + getResources().getDimension(R.dimen.labels_text_size) + ) + mLabelsCornerRadius = attr.getDimensionPixelSize( + R.styleable.FloatingActionMenu_menu_labels_cornerRadius, + mLabelsCornerRadius + ) + mLabelsShowShadow = + attr.getBoolean(R.styleable.FloatingActionMenu_menu_labels_showShadow, true) + mLabelsColorNormal = + attr.getColor(R.styleable.FloatingActionMenu_menu_labels_colorNormal, -0xcccccd) + mLabelsColorPressed = + attr.getColor(R.styleable.FloatingActionMenu_menu_labels_colorPressed, -0xbbbbbc) + mLabelsColorRipple = + attr.getColor(R.styleable.FloatingActionMenu_menu_labels_colorRipple, 0x66FFFFFF) + mMenuShowShadow = attr.getBoolean(R.styleable.FloatingActionMenu_menu_showShadow, true) + mMenuShadowColor = + attr.getColor(R.styleable.FloatingActionMenu_menu_shadowColor, 0x66000000) + mMenuShadowRadius = + attr.getDimension(R.styleable.FloatingActionMenu_menu_shadowRadius, mMenuShadowRadius) + mMenuShadowXOffset = + attr.getDimension(R.styleable.FloatingActionMenu_menu_shadowXOffset, mMenuShadowXOffset) + mMenuShadowYOffset = + attr.getDimension(R.styleable.FloatingActionMenu_menu_shadowYOffset, mMenuShadowYOffset) + mMenuColorNormal = attr.getColor(R.styleable.FloatingActionMenu_menu_colorNormal, -0x25bcca) + mMenuColorPressed = + attr.getColor(R.styleable.FloatingActionMenu_menu_colorPressed, -0x18afbd) + mMenuColorRipple = + attr.getColor(R.styleable.FloatingActionMenu_menu_colorRipple, -0x66000001) + this.animationDelayPerItem = + attr.getInt(R.styleable.FloatingActionMenu_menu_animationDelayPerItem, 50) + mIcon = attr.getDrawable(R.styleable.FloatingActionMenu_menu_icon) + if (mIcon == null) { + mIcon = getResources().getDrawable(R.drawable.fab_add) + } + mLabelsSingleLine = + attr.getBoolean(R.styleable.FloatingActionMenu_menu_labels_singleLine, false) + mLabelsEllipsize = attr.getInt(R.styleable.FloatingActionMenu_menu_labels_ellipsize, 0) + mLabelsMaxLines = attr.getInt(R.styleable.FloatingActionMenu_menu_labels_maxLines, -1) + mMenuFabSize = attr.getInt( + R.styleable.FloatingActionMenu_menu_fab_size, + FloatingActionButton.SIZE_NORMAL + ) + mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionMenu_menu_labels_style, 0) + val customFont = attr.getString(R.styleable.FloatingActionMenu_menu_labels_customFont) + try { + if (!TextUtils.isEmpty(customFont)) { + mCustomTypefaceFromFont = + Typeface.createFromAsset(getContext().getAssets(), customFont) + } + } catch (ex: RuntimeException) { + throw IllegalArgumentException( + "Unable to load specified custom font: " + customFont, + ex + ) + } + mOpenDirection = attr.getInt(R.styleable.FloatingActionMenu_menu_openDirection, OPEN_UP) + mBackgroundColor = + attr.getColor(R.styleable.FloatingActionMenu_menu_backgroundColor, Color.TRANSPARENT) + + if (attr.hasValue(R.styleable.FloatingActionMenu_menu_fab_label)) { + mUsingMenuLabel = true + mMenuLabelText = attr.getString(R.styleable.FloatingActionMenu_menu_fab_label) + } + + if (attr.hasValue(R.styleable.FloatingActionMenu_menu_labels_padding)) { + val padding = + attr.getDimensionPixelSize(R.styleable.FloatingActionMenu_menu_labels_padding, 0) + initPadding(padding) + } + + mOpenInterpolator = OvershootInterpolator() + mCloseInterpolator = AnticipateInterpolator() + mLabelsContext = ContextThemeWrapper(getContext(), mLabelsStyle) + + isDragMenuDisabled = false + + initBackgroundDimAnimation() + createMenuButton() + initMenuButtonAnimations(attr) + + attr.recycle() + } + + private fun initMenuButtonAnimations(attr: TypedArray) { + val showResId = attr.getResourceId( + R.styleable.FloatingActionMenu_menu_fab_show_animation, + R.anim.fab_scale_up + ) + setMenuButtonShowAnimation(AnimationUtils.loadAnimation(getContext(), showResId)) + mImageToggleShowAnimation = AnimationUtils.loadAnimation(getContext(), showResId) + + val hideResId = attr.getResourceId( + R.styleable.FloatingActionMenu_menu_fab_hide_animation, + R.anim.fab_scale_down + ) + setMenuButtonHideAnimation(AnimationUtils.loadAnimation(getContext(), hideResId)) + mImageToggleHideAnimation = AnimationUtils.loadAnimation(getContext(), hideResId) + } + + private fun initBackgroundDimAnimation() { + val maxAlpha = Color.alpha(mBackgroundColor) + val red = Color.red(mBackgroundColor) + val green = Color.green(mBackgroundColor) + val blue = Color.blue(mBackgroundColor) + + mShowBackgroundAnimator = ValueAnimator.ofInt(0, maxAlpha) + mShowBackgroundAnimator!!.setDuration(ANIMATION_DURATION.toLong()) + mShowBackgroundAnimator!!.addUpdateListener(object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + val alpha = animation.getAnimatedValue() as Int + setBackgroundColor(Color.argb(alpha, red, green, blue)) + } + }) + + mHideBackgroundAnimator = ValueAnimator.ofInt(maxAlpha, 0) + mHideBackgroundAnimator!!.setDuration(ANIMATION_DURATION.toLong()) + mHideBackgroundAnimator!!.addUpdateListener(object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + val alpha = animation.getAnimatedValue() as Int + setBackgroundColor(Color.argb(alpha, red, green, blue)) + } + }) + } + + private val isBackgroundEnabled: Boolean + get() = mBackgroundColor != Color.TRANSPARENT + + private fun initPadding(padding: Int) { + mLabelsPaddingTop = padding + mLabelsPaddingRight = padding + mLabelsPaddingBottom = padding + mLabelsPaddingLeft = padding + } + + private fun createMenuButton() { + mMenuButton = FloatingActionButton(getContext()) + + mMenuButton!!.mShowShadow = mMenuShowShadow + if (mMenuShowShadow) { + mMenuButton!!.mShadowRadius = CommonUtils.dpToPx(getContext(), mMenuShadowRadius) + mMenuButton!!.mShadowXOffset = CommonUtils.dpToPx(getContext(), mMenuShadowXOffset) + mMenuButton!!.mShadowYOffset = CommonUtils.dpToPx(getContext(), mMenuShadowYOffset) + } + mMenuButton!!.setColors(mMenuColorNormal, mMenuColorPressed, mMenuColorRipple) + mMenuButton!!.mShadowColor = mMenuShadowColor + mMenuButton!!.mFabSize = mMenuFabSize + mMenuButton!!.updateBackground() + mMenuButton!!.labelText = mMenuLabelText + + mImageToggle = ImageView(getContext()) + mImageToggle?.scaleType = ScaleType.FIT_CENTER + mImageToggle?.adjustViewBounds = true + mImageToggle!!.setImageDrawable(mIcon) + + addView(mMenuButton, super.generateDefaultLayoutParams()) + addView(mImageToggle) + + createDefaultIconAnimation() + } + + private fun createDefaultIconAnimation() { + val collapseAngle: Float + val expandAngle: Float + if (mOpenDirection == OPEN_UP) { + collapseAngle = + if (mLabelsPosition == LABELS_POSITION_LEFT) OPENED_PLUS_ROTATION_LEFT else OPENED_PLUS_ROTATION_RIGHT + expandAngle = + if (mLabelsPosition == LABELS_POSITION_LEFT) OPENED_PLUS_ROTATION_LEFT else OPENED_PLUS_ROTATION_RIGHT + } else { + collapseAngle = + if (mLabelsPosition == LABELS_POSITION_LEFT) OPENED_PLUS_ROTATION_RIGHT else OPENED_PLUS_ROTATION_LEFT + expandAngle = + if (mLabelsPosition == LABELS_POSITION_LEFT) OPENED_PLUS_ROTATION_RIGHT else OPENED_PLUS_ROTATION_LEFT + } + + val collapseAnimator = ObjectAnimator.ofFloat( + mImageToggle, + "rotation", + collapseAngle, + CLOSED_PLUS_ROTATION + ) + + val expandAnimator = ObjectAnimator.ofFloat( + mImageToggle, + "rotation", + CLOSED_PLUS_ROTATION, + expandAngle + ) + + mOpenAnimatorSet.play(expandAnimator) + mCloseAnimatorSet.play(collapseAnimator) + + mOpenAnimatorSet.setInterpolator(mOpenInterpolator) + mCloseAnimatorSet.setInterpolator(mCloseInterpolator) + + mOpenAnimatorSet.setDuration(ANIMATION_DURATION.toLong()) + mCloseAnimatorSet.setDuration(ANIMATION_DURATION.toLong()) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + var width = 0 + var height = 0 + mMaxButtonWidth = 0 + var maxLabelWidth = 0 + + measureChildWithMargins(mImageToggle, widthMeasureSpec, 0, heightMeasureSpec, 0) + + for (i in 0.. { + dX = v.getX() - event.getRawX() + dY = v.getY() - event.getRawY() + startX = event.getRawX() + startY = event.getRawY() + lastAction = MotionEvent.ACTION_DOWN + } + + MotionEvent.ACTION_MOVE -> { + if (!isDragMenuDisabled and (checkViewLimits( + v, + (event.getRawX() + dX).toInt(), + (event.getRawY() + dY).toInt() + )) + ) { + v.setY(event.getRawY() + dY) + v.setX(event.getRawX() + dX) + + moveElements( + v, + event.getRawX() + dX, + event.getRawY() + dY, + oldX, + oldY + ) + } + lastAction = MotionEvent.ACTION_MOVE + } + + MotionEvent.ACTION_UP -> if (abs((startX - event.getRawX()).toDouble()) < 10 && abs( + (startY - event.getRawY()).toDouble() + ) < 10 + ) { + toggle(mIsAnimated) + } + + else -> return false + } + return true + } + }) + } + } + } + + private fun checkViewLimits(v: View?, setX: Int, setY: Int): Boolean { + val parent = (this.getParent() as ViewGroup).getParent() as View + + val l = setX + val t = setY + val r = l + mMenuButton!!.getMeasuredWidth() + val b = t + mMenuButton!!.getMeasuredHeight() + + val height = parent.getHeight() + val width = parent.getWidth() + + if ((r + getPaddingRight()) > width || l < 0 || t < 0 || (b + getPaddingBottom()) > height) { + return false + } else { + return true + } + } + + private fun moveElements(v: View?, SetX: Float, SetY: Float, oldX: Float, oldY: Float) { + val parent = (this.getParent() as ViewGroup).getParent() as View + + val l = mMenuButton!!.getX().toInt() + val t = mMenuButton!!.getY().toInt() + val r = l + mMenuButton!!.getMeasuredWidth() + val b = t + mMenuButton!!.getMeasuredHeight() + + val mMenuH = mMenuButton!!.getMeasuredHeight() + + val halfHeight = parent.getHeight() / 2 + + val buttonsHorizontalCenter = (r - l) / 2 + val openUp = halfHeight < (t) + + var nextY = if (openUp) t - getPaddingTop() else t + mMenuH + var fab: Any? + + for (i in mButtonsCount - 1 downTo 0) { + val child = getChildAt(i) + if (child.getVisibility() == GONE) continue + + val childX: Int + val childY: Int + + if (child === mImageToggle) { + val imageLeft = l + buttonsHorizontalCenter - mImageToggle!!.getMeasuredWidth() / 2 + val imageTop = + t + mMenuButton!!.getMeasuredHeight() / 2 - mImageToggle!!.getMeasuredHeight() / 2 + child.layout( + imageLeft, + imageTop, + l + mImageToggle!!.getMeasuredWidth() * 2, + t + mImageToggle!!.getMeasuredHeight() * 2 + ) + } else { + childX = l + buttonsHorizontalCenter - child.getMeasuredWidth() / 2 + childY = + if (openUp) nextY - child.getMeasuredHeight() - mButtonSpacing else nextY + mButtonSpacing + + fab = child as FloatingActionButton + if (fab === mMenuButton) continue + + // only child buttons and imageToggle on menuBtn. + child.layout( + childX, childY, childX + child.getMeasuredWidth(), + childY + child.getMeasuredHeight() + ) + + if (!mIsMenuOpening) { + if (child is FloatingActionButton) fab.hide(false) + } + + var label: View? = null + if (child !== mImageToggle) label = fab.getTag(R.id.fab_label) as View? + + if (label != null) { + val labelsOffset = + (if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin + val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT) + l + buttonsHorizontalCenter - labelsOffset + else + l + buttonsHorizontalCenter + labelsOffset + + val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT) + labelXNearButton - label.getMeasuredWidth() + else + labelXNearButton + label.getMeasuredWidth() + + val labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT) + labelXAwayFromButton + else + labelXNearButton + + val labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT) + labelXNearButton + else + labelXAwayFromButton + + val labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() + - label.getMeasuredHeight()) / 2 + + label.layout( + labelLeft, + labelTop, + labelRight, + labelTop + label.getMeasuredHeight() + ) + + if (!mIsMenuOpening) { + label.setVisibility(INVISIBLE) + } + } + nextY = + if (openUp) childY - mButtonSpacing else childY + fab.getMeasuredHeight() + mButtonSpacing + } + } + } + + private fun addLabel(fab: FloatingActionButton) { + val text = fab.labelText + + if (TextUtils.isEmpty(text)) return + + val label: Label = Label(mLabelsContext) + label.setClickable(true) + label.setFab(fab) + label.setShowAnimation(AnimationUtils.loadAnimation(getContext(), mLabelsShowAnimation)) + label.setHideAnimation(AnimationUtils.loadAnimation(getContext(), mLabelsHideAnimation)) + + if (mLabelsStyle > 0) { + label.setTextAppearance(getContext(), mLabelsStyle) + label.setShowShadow(false) + label.setUsingStyle(true) + } else { + label.setColors(mLabelsColorNormal, mLabelsColorPressed, mLabelsColorRipple) + label.setShowShadow(mLabelsShowShadow) + label.setCornerRadius(mLabelsCornerRadius) + if (mLabelsEllipsize > 0) { + setLabelEllipsize(label) + } + label.setMaxLines(mLabelsMaxLines) + label.updateBackground() + + label.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLabelsTextSize) + label.setTextColor(mLabelsTextColor) + + var left = mLabelsPaddingLeft + var top = mLabelsPaddingTop + if (mLabelsShowShadow) { + left += (fab.shadowRadius + abs(fab.shadowXOffset.toDouble())).toInt() + top += (fab.shadowRadius + abs(fab.shadowYOffset.toDouble())).toInt() + } + + label.setPadding( + left, + top, + mLabelsPaddingLeft, + mLabelsPaddingTop + ) + + if (mLabelsMaxLines < 0 || mLabelsSingleLine) { + label.setSingleLine(mLabelsSingleLine) + } + } + + if (mCustomTypefaceFromFont != null) { + label.setTypeface(mCustomTypefaceFromFont) + } + label.setText(text) + label.setOnClickListener(fab.getOnClickListener()) + + addView(label) + fab.setTag(R.id.fab_label, label) + } + + private fun setLabelEllipsize(label: Label) { + when (mLabelsEllipsize) { + 1 -> label.setEllipsize(TextUtils.TruncateAt.START) + 2 -> label.setEllipsize(TextUtils.TruncateAt.MIDDLE) + 3 -> label.setEllipsize(TextUtils.TruncateAt.END) + 4 -> label.setEllipsize(TextUtils.TruncateAt.MARQUEE) + } + } + + override fun generateLayoutParams(attrs: AttributeSet?): MarginLayoutParams { + return MarginLayoutParams(getContext(), attrs) + } + + override fun generateLayoutParams(p: LayoutParams?): MarginLayoutParams { + return MarginLayoutParams(p) + } + + override fun generateDefaultLayoutParams(): MarginLayoutParams { + return MarginLayoutParams( + MarginLayoutParams.WRAP_CONTENT, + MarginLayoutParams.WRAP_CONTENT + ) + } + + override fun checkLayoutParams(p: LayoutParams?): Boolean { + return p is MarginLayoutParams + } + + private fun hideMenuButtonWithImage(animate: Boolean) { + if (!this.isMenuButtonHidden) { + mMenuButton!!.hide(animate) + if (animate) { + mImageToggle!!.startAnimation(mImageToggleHideAnimation) + } + mImageToggle!!.setVisibility(INVISIBLE) + mIsMenuButtonAnimationRunning = false + } + } + + private fun showMenuButtonWithImage(animate: Boolean) { + if (this.isMenuButtonHidden) { + mMenuButton!!.show(animate) + if (animate) { + mImageToggle!!.startAnimation(mImageToggleShowAnimation) + } + mImageToggle!!.setVisibility(VISIBLE) + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + val dX = 0f + val dY = 0f + val startX = 0f + val startY = 0f + var lastAction: Int + val v = mMenuButton as View? + // click event + if (mIsSetClosedOnTouchOutside) { + var handled = false + when (event.getAction()) { + MotionEvent.ACTION_DOWN -> handled = this.isOpened + MotionEvent.ACTION_UP -> { + close(mIsAnimated) + handled = true + } + } + + return handled + } + + return super.onTouchEvent(event) + } + + fun toggle(animate: Boolean) { + if (this.isOpened) { + close(animate) + } else { + open(animate) + } + } + + fun open(animate: Boolean) { + if (!this.isOpened) { + if (this.isBackgroundEnabled) { + mShowBackgroundAnimator!!.start() + } + + if (this.isIconAnimated) { + if (this.iconToggleAnimatorSet != null) { + iconToggleAnimatorSet!!.start() + } else { + mCloseAnimatorSet.cancel() + mOpenAnimatorSet.start() + } + } + + var delay = 0 + var counter = 0 + mIsMenuOpening = true + for (i in getChildCount() - 1 downTo 0) { + val child = getChildAt(i) + if (child is FloatingActionButton && child.getVisibility() != GONE) { + counter++ + + val fab = child + mUiHandler.postDelayed(object : Runnable { + override fun run() { + if (this@FloatingActionMenu.isOpened) return + + if (fab != mMenuButton) { + fab.show(animate) + } + + val label: Label? = fab.getTag(R.id.fab_label) as Label? + if (label != null && label.isHandleVisibilityChanges) { + label.show(animate) + } + } + }, delay.toLong()) + delay += this.animationDelayPerItem + } + } + + mUiHandler.postDelayed(object : Runnable { + override fun run() { + this@FloatingActionMenu.isOpened = true + + if (mToggleListener != null) { + mToggleListener!!.onMenuToggle(true) + } + } + }, (++counter * this.animationDelayPerItem).toLong()) + } + } + + fun close(animate: Boolean) { + if (this.isOpened) { + if (this.isBackgroundEnabled) { + mHideBackgroundAnimator!!.start() + } + + if (this.isIconAnimated) { + if (this.iconToggleAnimatorSet != null) { + iconToggleAnimatorSet!!.start() + } else { + mCloseAnimatorSet.start() + mOpenAnimatorSet.cancel() + } + } + + var delay = 0 + var counter = 0 + mIsMenuOpening = false + for (i in 0.. size) { + index = size + } + + addView(fab, index) + mButtonsCount++ + addLabel(fab) + } + + fun removeAllMenuButtons() { + close(true) + + val viewsToRemove: MutableList = ArrayList() + for (i in 0..( + this.Shadow(), + createFillDrawable() + ) + ) + + val leftInset: Int = (mShadowRadius + abs(mShadowXOffset.toDouble())).toInt() + val topInset: Int = (mShadowRadius + abs(mShadowYOffset.toDouble())).toInt() + val rightInset: Int = ((mShadowRadius + abs(mShadowXOffset.toDouble()))).toInt() + val bottomInset: Int = ((mShadowRadius + abs(mShadowYOffset.toDouble()))).toInt() + + layerDrawable.setLayerInset( + 1, + leftInset, + topInset, + rightInset, + bottomInset + ) + } else { + layerDrawable = LayerDrawable( + arrayOf( + createFillDrawable() + ) + ) + } + + setBackgroundCompat(layerDrawable) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun createFillDrawable(): Drawable { + val drawable = StateListDrawable() + drawable.addState(intArrayOf(android.R.attr.state_pressed), createRectDrawable(mColorPressed)) + drawable.addState(intArrayOf(), createRectDrawable(mColorNormal)) + + if (CommonUtils.hasLollipop()) { + val ripple = RippleDrawable( + ColorStateList( + arrayOf(intArrayOf()), + intArrayOf(mColorRipple) + ), drawable, null + ) + setOutlineProvider(object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()) + } + }) + setClipToOutline(true) + mBackgroundDrawable = ripple + return ripple + } + + mBackgroundDrawable = drawable + return drawable + } + + private fun createRectDrawable(color: Int): Drawable { + val shape = RoundRectShape( + floatArrayOf( + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mCornerRadius.toFloat() + ), + null, + null + ) + val shapeDrawable = ShapeDrawable(shape) + shapeDrawable.getPaint().setColor(color) + return shapeDrawable + } + + private fun setShadow(fab: FloatingActionButton) { + mShadowColor = fab.shadowColor + mShadowRadius = fab.shadowRadius + mShadowXOffset = fab.shadowXOffset + mShadowYOffset = fab.shadowYOffset + mShowShadow = fab.hasShadow() + } + + @Suppress("deprecation") + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun setBackgroundCompat(drawable: Drawable?) { + if (CommonUtils.hasJellyBean()) { + setBackground(drawable) + } else { + setBackgroundDrawable(drawable) + } + } + + private fun playShowAnimation() { + if (mShowAnimation != null) { + mHideAnimation!!.cancel() + startAnimation(mShowAnimation) + } + } + + private fun playHideAnimation() { + if (mHideAnimation != null) { + mShowAnimation!!.cancel() + startAnimation(mHideAnimation) + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + fun onActionDown() { + if (mUsingStyle) { + mBackgroundDrawable = getBackground() + } + + if (mBackgroundDrawable is StateListDrawable) { + val drawable = mBackgroundDrawable as StateListDrawable + drawable.setState(intArrayOf(android.R.attr.state_pressed)) + } else if (CommonUtils.hasLollipop() && mBackgroundDrawable is RippleDrawable) { + val ripple = mBackgroundDrawable as RippleDrawable + ripple.setState(intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed)) + ripple.setHotspot( + (getMeasuredWidth() / 2).toFloat(), + (getMeasuredHeight() / 2).toFloat() + ) + ripple.setVisible(true, true) + } + // setPressed(true); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + fun onActionUp() { + if (mUsingStyle) { + mBackgroundDrawable = getBackground() + } + + if (mBackgroundDrawable is StateListDrawable) { + val drawable = mBackgroundDrawable as StateListDrawable + drawable.setState(intArrayOf()) + } else if (CommonUtils.hasLollipop() && mBackgroundDrawable is RippleDrawable) { + val ripple = mBackgroundDrawable as RippleDrawable + ripple.setState(intArrayOf()) + ripple.setHotspot( + (getMeasuredWidth() / 2).toFloat(), + (getMeasuredHeight() / 2).toFloat() + ) + ripple.setVisible(true, true) + } + // setPressed(false); + } + + fun setFab(fab: FloatingActionButton) { + mFab = fab + setShadow(fab) + } + + fun setShowShadow(show: Boolean) { + mShowShadow = show + } + + fun setCornerRadius(cornerRadius: Int) { + mCornerRadius = cornerRadius + } + + fun setColors(colorNormal: Int, colorPressed: Int, colorRipple: Int) { + mColorNormal = colorNormal + mColorPressed = colorPressed + mColorRipple = colorRipple + } + + fun show(animate: Boolean) { + if (animate) { + playShowAnimation() + } + setVisibility(VISIBLE) + } + + fun hide(animate: Boolean) { + if (animate) { + playHideAnimation() + } + setVisibility(INVISIBLE) + } + + fun setShowAnimation(showAnimation: Animation?) { + mShowAnimation = showAnimation + } + + fun setHideAnimation(hideAnimation: Animation?) { + mHideAnimation = hideAnimation + } + + fun setUsingStyle(usingStyle: Boolean) { + mUsingStyle = usingStyle + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (mFab == null || mFab!!.getOnClickListener() == null || !mFab!!.isEnabled()) { + return super.onTouchEvent(event) + } + + val action = event.getAction() + when (action) { + MotionEvent.ACTION_UP -> { + onActionUp() + mFab!!.onActionUp() + } + + MotionEvent.ACTION_CANCEL -> { + onActionUp() + mFab!!.onActionUp() + } + } + + mGestureDetector.onTouchEvent(event) + return super.onTouchEvent(event) + } + + var mGestureDetector: GestureDetector = + GestureDetector(getContext(), object : SimpleOnGestureListener() { + override fun onDown(e: MotionEvent): Boolean { + onActionDown() + if (mFab != null) { + mFab!!.onActionDown() + } + return super.onDown(e) + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + onActionUp() + if (mFab != null) { + mFab!!.onActionUp() + } + return super.onSingleTapUp(e) + } + }) + + private inner class Shadow : Drawable { + + private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val mErase = Paint(Paint.ANTI_ALIAS_FLAG) + + constructor() : super() + + init { + this.init() + } + + fun init() { + setLayerType(LAYER_TYPE_SOFTWARE, null) + mPaint.setStyle(Paint.Style.FILL) + mPaint.setColor(mColorNormal) + + mErase.setXfermode(PORTER_DUFF_CLEAR) + + if (!isInEditMode()) { + mPaint.setShadowLayer( + mShadowRadius.toFloat(), + mShadowXOffset.toFloat(), + mShadowYOffset.toFloat(), + mShadowColor + ) + } + } + + override fun draw(canvas: Canvas) { + val shadowRect = RectF( + (mShadowRadius + abs(mShadowXOffset.toDouble())).toFloat(), + (mShadowRadius + abs(mShadowYOffset.toDouble())).toFloat(), + mRawWidth.toFloat(), + mRawHeight.toFloat() + ) + + canvas.drawRoundRect( + shadowRect, + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mPaint + ) + canvas.drawRoundRect( + shadowRect, + mCornerRadius.toFloat(), + mCornerRadius.toFloat(), + mErase + ) + } + + override fun setAlpha(alpha: Int) { + + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + + } + + override fun getOpacity(): Int { + return 0 + } + + } + + companion object { + private val PORTER_DUFF_CLEAR: Xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/fab_scale_down.xml b/app/src/main/res/anim/fab_scale_down.xml new file mode 100644 index 00000000..7c7edb55 --- /dev/null +++ b/app/src/main/res/anim/fab_scale_down.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_scale_up.xml b/app/src/main/res/anim/fab_scale_up.xml new file mode 100644 index 00000000..178c4dac --- /dev/null +++ b/app/src/main/res/anim/fab_scale_up.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_slide_in_from_left.xml b/app/src/main/res/anim/fab_slide_in_from_left.xml new file mode 100644 index 00000000..2e4d2e2f --- /dev/null +++ b/app/src/main/res/anim/fab_slide_in_from_left.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_slide_in_from_right.xml b/app/src/main/res/anim/fab_slide_in_from_right.xml new file mode 100644 index 00000000..b24e102b --- /dev/null +++ b/app/src/main/res/anim/fab_slide_in_from_right.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_slide_out_to_left.xml b/app/src/main/res/anim/fab_slide_out_to_left.xml new file mode 100644 index 00000000..00ddfa17 --- /dev/null +++ b/app/src/main/res/anim/fab_slide_out_to_left.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fab_slide_out_to_right.xml b/app/src/main/res/anim/fab_slide_out_to_right.xml new file mode 100644 index 00000000..f3ba380e --- /dev/null +++ b/app/src/main/res/anim/fab_slide_out_to_right.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fab_add.png b/app/src/main/res/drawable/fab_add.png new file mode 100644 index 00000000..c1180720 Binary files /dev/null and b/app/src/main/res/drawable/fab_add.png differ diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index e3ca1f42..a120e345 100644 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -1,7 +1,7 @@ + android:tint="@color/Color_White"> diff --git a/app/src/main/res/layout/launcher_activity.xml b/app/src/main/res/layout/launcher_activity.xml index adc2adf5..d6646ed5 100644 --- a/app/src/main/res/layout/launcher_activity.xml +++ b/app/src/main/res/layout/launcher_activity.xml @@ -19,44 +19,64 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" - app:layout_constraintBottom_toTopOf="@id/tabs_sc"/> + app:layout_constraintBottom_toTopOf="@id/current_address"/> - + + - - - - - - - - - + - - - - - - - - - + diff --git a/app/src/main/res/layout/launcher_home.xml b/app/src/main/res/layout/launcher_home.xml index f2fd9013..712b279a 100644 --- a/app/src/main/res/layout/launcher_home.xml +++ b/app/src/main/res/layout/launcher_home.xml @@ -23,23 +23,10 @@ android:foregroundTint="@color/finestSilver" tools:ignore="ContentDescription,UseAppTint" android:layout_height="30dp" /> + - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ab48c87f..6a04364f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -39,4 +39,9 @@ 30dp 15dp 45dp + + 56dp + 40dp + 14sp + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 00000000..28a9fb13 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file