瀏覽代碼

Merge pull request #1 from MrOzOn/custom_view_animations

Custom view animations
MrOzOn 5 年之前
父節點
當前提交
cefc85e2de

+ 2 - 2
core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt

@@ -6,8 +6,8 @@ import retrofit2.http.*
 
 interface HealthDiaryService {
     companion object {
-        const val ENDPOINT = "http://10.0.2.2:8000/"
-//        const val ENDPOINT = "https://hdb.mr-ozon-1982.tk/"
+//        const val ENDPOINT = "http://10.0.2.2:8000/"
+        const val ENDPOINT = "https://hdb.mr-ozon-1982.tk/"
     }
 
     @POST("login/")

+ 3 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragment.kt

@@ -3,6 +3,7 @@ package com.mrozon.feature_auth.presentation
 import android.content.Context
 import android.os.Bundle
 import android.view.View
+import android.view.animation.AnimationUtils
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
@@ -14,6 +15,7 @@ import com.mrozon.feature_auth.di.LoginFragmentComponent
 import com.mrozon.utils.base.BaseFragment
 import com.mrozon.utils.extension.hideKeyboard
 import com.mrozon.utils.extension.offer
+import com.mrozon.utils.extension.shake
 import com.mrozon.utils.extension.visible
 import com.mrozon.utils.network.Result
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -80,6 +82,7 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
                     }
                     Result.Status.ERROR -> {
                         binding?.progressBar?.visible(false)
+                        binding?.btnLogin?.shake()
                         showError(result.message!!)
                     }
                 }

+ 2 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragment.kt

@@ -16,6 +16,7 @@ import com.mrozon.feature_auth.di.RegistrationFragmentComponent
 import com.mrozon.utils.base.BaseFragment
 import com.mrozon.utils.extension.hideKeyboard
 import com.mrozon.utils.extension.offer
+import com.mrozon.utils.extension.shake
 import com.mrozon.utils.extension.visible
 import com.mrozon.utils.network.Result
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -84,6 +85,7 @@ class RegistrationFragment: BaseFragment<FragmentRegistrationBinding>() {
                     }
                     Result.Status.ERROR -> {
                         binding?.progressBar?.visible(false)
+                        binding?.btnRegistration?.shake()
                         showError(result.message!!)
                     }
                 }

+ 169 - 0
feature_person/src/main/java/com/mrozon/feature_person/customview/GenderSwitch.kt

@@ -0,0 +1,169 @@
+package com.mrozon.feature_person.customview
+
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Build
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import com.mrozon.feature_person.R
+import timber.log.Timber
+
+
+class GenderSwitch(context: Context, attrs: AttributeSet?) : View(context, attrs) {
+
+    private val paintChecked : Paint = Paint()
+    private val paintUnchecked : Paint = Paint()
+    private var mIsMale :  Boolean = true
+
+    private var listener: OnGenderChangeListener? = null
+
+    fun isMale(): Boolean {
+        return mIsMale
+    }
+
+    fun setMale(male: Boolean) {
+        mIsMale = male
+        invalidate()
+        requestLayout()
+    }
+
+    init {
+        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.GenderSwitch)
+        try {
+            paintChecked.color = typedArray.getColor(R.styleable.GenderSwitch_colorChecked,
+                Color.parseColor("#03A9F4"))
+            paintUnchecked.color = typedArray.getColor(R.styleable.GenderSwitch_colorUnchecked,
+                Color.parseColor("#E0E0E0"))
+            mIsMale = typedArray.getBoolean(R.styleable.GenderSwitch_isMale, true)
+        } finally {
+            typedArray.recycle()
+        }
+    }
+
+    fun setOnGenderChangeListener(listener: OnGenderChangeListener){
+        this.listener = listener
+    }
+
+    private val paintActive : Paint = Paint().apply {
+    }
+
+    private val paintInactive : Paint = Paint().apply {
+        alpha = 75
+    }
+
+    private var bitmapMale:Bitmap? = null
+    private var bitmapFemale:Bitmap? = null
+    private var sizeBitmap = 0
+
+    private var maleXPosition: Float = 0f
+    private var femaleXPosition: Float = 0f
+    private var currentXPosition: Float = 0f
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        Timber.d("onMeasure")
+
+        val heightSize = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom
+        val widthSize = (MeasureSpec.getSize(widthMeasureSpec) - 2 * paddingStart - 2 * paddingEnd) / 2.5
+        sizeBitmap = minOf(heightSize, widthSize.toInt())
+        bitmapMale = getBitmapFromVectorDrawable(context, R.drawable.ic_male_avatar,
+            sizeBitmap)
+        bitmapFemale = getBitmapFromVectorDrawable(context, R.drawable.ic_female_avatar,
+            sizeBitmap)
+        maleXPosition = sizeBitmap.toFloat()+paddingStart+paddingEnd
+        femaleXPosition = MeasureSpec.getSize(widthMeasureSpec) - maleXPosition
+        if(mIsMale)
+            currentXPosition = 0f
+        else
+            currentXPosition = maleXPosition
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        Timber.d("onLayout")
+        super.onLayout(changed, left, top, right, bottom)
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        Timber.d("onDraw")
+        super.onDraw(canvas)
+
+        val top = (height - sizeBitmap)/2F
+        val widthCheckedArea = width-(sizeBitmap+paddingStart+paddingEnd*1F)
+
+        canvas.apply {
+            drawRect(0f,0f,width.toFloat(), height.toFloat(), paintUnchecked)
+            drawBitmap(bitmapMale!!,(paddingStart+paddingEnd)/2f,top,paintInactive)
+            drawBitmap(bitmapFemale!!,widthCheckedArea + (paddingStart+paddingEnd)/2F,top,paintInactive)
+            drawRect(currentXPosition, 0f, currentXPosition+widthCheckedArea, height.toFloat(), paintChecked)
+
+            var bitmap = bitmapMale!!
+            if(!mIsMale)
+                bitmap = bitmapFemale!!
+            drawBitmap(bitmap,currentXPosition + (widthCheckedArea-sizeBitmap)/2,top,paintActive)
+        }
+    }
+
+    @SuppressLint("ObsoleteSdkInt")
+    private fun getBitmapFromVectorDrawable(context: Context?, drawableId: Int, width:Int): Bitmap {
+        val drawable = ContextCompat.getDrawable(context!!, drawableId)
+        val bitmap = Bitmap.createBitmap(
+            width,
+            width,
+            Bitmap.Config.ARGB_8888
+        )
+        val canvas = Canvas(bitmap)
+        drawable?.setBounds(0, 0, canvas.width, canvas.height)
+        drawable?.draw(canvas)
+        return bitmap
+    }
+
+    private val myListener =  object : GestureDetector.SimpleOnGestureListener() {
+        override fun onDown(e: MotionEvent): Boolean {
+            Timber.d("onDown: $e")
+            return true
+        }
+    }
+    private val detector: GestureDetector = GestureDetector(context, myListener)
+
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        return detector.onTouchEvent(event).let { result ->
+            if (!result) {
+                if (event?.action == MotionEvent.ACTION_UP) {
+//                    Timber.d("ACTION_UP: $event")
+                    mIsMale = !mIsMale
+                    listener?.onMaleSelected(mIsMale)
+                    val startValueAnimator = currentXPosition
+                    var endValueAnimator = 0f
+                    if(!mIsMale){
+//                        startValueAnimator = 0f
+                        endValueAnimator = maleXPosition
+                    }
+                    val valueAnimator = ValueAnimator.ofFloat(startValueAnimator, endValueAnimator).apply {
+//                        duration = 1000
+                        addUpdateListener {
+                            currentXPosition = it.animatedValue as Float
+//                            Timber.d("animatedValue=${it.animatedValue}")
+                            invalidate()
+                        }
+                    }
+                    valueAnimator.start()
+                    true
+                } else false
+            } else true
+        }
+    }
+
+    interface OnGenderChangeListener {
+        fun onMaleSelected(male: Boolean)
+    }
+
+}

+ 31 - 36
feature_person/src/main/java/com/mrozon/feature_person/presentation/EditPersonFragment.kt

@@ -2,20 +2,18 @@ package com.mrozon.feature_person.presentation
 
 import android.app.AlertDialog
 import android.content.Context
-import android.content.DialogInterface
 import android.os.Bundle
 import android.view.*
 import android.widget.EditText
 import androidx.core.app.ActivityCompat.invalidateOptionsMenu
-import androidx.core.view.get
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
 import androidx.navigation.fragment.findNavController
 import com.mrozon.core_api.entity.Gender
 import com.mrozon.core_api.navigation.EditPersonNavigator
-import com.mrozon.core_api.navigation.ListPersonNavigator
 import com.mrozon.feature_person.R
+import com.mrozon.feature_person.customview.GenderSwitch
 import com.mrozon.feature_person.databinding.FragmentEditPersonBinding
 import com.mrozon.feature_person.di.EditPersonFragmentComponent
 import com.mrozon.utils.base.BaseFragment
@@ -23,7 +21,6 @@ import com.mrozon.utils.extension.*
 import com.mrozon.utils.network.Result
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.FlowPreview
-import timber.log.Timber
 import java.util.*
 import javax.inject.Inject
 
@@ -58,16 +55,17 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        binding?.radioGroup?.setOnCheckedChangeListener { _, id ->
-            if(id==R.id.rbMale)
-                viewModel.setMaleGender(true)
-            else
-                viewModel.setMaleGender(false)
-        }
+        binding?.genderSwitch?.setOnGenderChangeListener(object : GenderSwitch.OnGenderChangeListener{
+            override fun onMaleSelected(male: Boolean) {
+                viewModel.setMaleGender(male)
+            }
+        })
+
+        binding?.genderSwitch?.isMale()
 
         binding?.etPersonName?.offer(viewModel.personNameChannel)
 
-        binding?.pickerBoD?.init(2020,1,1)
+        binding?.pickerBoD?.init(2020, 1, 1)
         { _, year, month, day ->
             val date = "$year-${month+1}-$day"
 //            Timber.d("string = $date date=${date.toSimpleDate().toDateString()}")
@@ -75,11 +73,13 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
         }
         val calendar = Calendar.getInstance()
         calendar.time = Date()
-        binding?.pickerBoD?.updateDate(calendar.get(Calendar.YEAR),
+        binding?.pickerBoD?.updateDate(
+            calendar.get(Calendar.YEAR),
             calendar.get(Calendar.MONTH),
-            calendar.get(Calendar.DAY_OF_MONTH))
+            calendar.get(Calendar.DAY_OF_MONTH)
+        )
 
-        val id = arguments?.getLong("id",0)?:0
+        val id = arguments?.getLong("id", 0)?:0
         if(id>0){
             viewModel.initValue(id)
         }
@@ -88,19 +88,13 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
     @ExperimentalCoroutinesApi
     @FlowPreview
     override fun subscribeUi() {
-        viewModel.male.observe(viewLifecycleOwner, Observer { isMale ->
-            if (isMale)
-                binding?.ivGenger?.setImageResource(R.drawable.ic_male_avatar)
-            else
-                binding?.ivGenger?.setImageResource(R.drawable.ic_female_avatar)
-        })
 
         viewModel.enableAdding.observe(viewLifecycleOwner, Observer { enabled ->
             invalidateOptionsMenu(activity)
         })
 
         viewModel.person.observe(viewLifecycleOwner, Observer { result ->
-            if(result!=null){
+            if (result != null) {
                 when (result.status) {
                     Result.Status.LOADING -> {
                         binding?.progressBar?.visible(true)
@@ -118,7 +112,7 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
         })
 
         viewModel.initPerson.observe(viewLifecycleOwner, Observer { result ->
-            if(result!=null){
+            if (result != null) {
                 when (result.status) {
                     Result.Status.LOADING -> {
                         binding?.progressBar?.visible(true)
@@ -127,17 +121,19 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
                         arguments?.remove("id")
                         binding?.progressBar?.visible(false)
                         binding?.etPersonName?.setText(result.data?.name)
-                        if (result.data?.gender==Gender.FEMALE)
-                            binding?.radioGroup?.check(R.id.rbFemale)
+                        if (result.data?.gender == Gender.FEMALE)
+                            binding?.genderSwitch?.setMale(false)
                         else
-                            binding?.radioGroup?.check(R.id.rbMale)
+                            binding?.genderSwitch?.setMale(true)
                         val calendar = Calendar.getInstance()
-                        calendar.time = result.data?.born?:Date()
-                        binding?.pickerBoD?.updateDate(calendar.get(Calendar.YEAR),
+                        calendar.time = result.data?.born ?: Date()
+                        binding?.pickerBoD?.updateDate(
+                            calendar.get(Calendar.YEAR),
                             calendar.get(Calendar.MONTH),
-                            calendar.get(Calendar.DAY_OF_MONTH))
+                            calendar.get(Calendar.DAY_OF_MONTH)
+                        )
                         viewModel.initDone()
-                        arguments?.putLong("current_id",result.data?.id?:-1)
+                        arguments?.putLong("current_id", result.data?.id ?: -1)
                         invalidateOptionsMenu(activity)
                     }
                     Result.Status.ERROR -> {
@@ -149,7 +145,7 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
         })
 
         viewModel.deletedPerson.observe(viewLifecycleOwner, Observer { result ->
-            if(result!=null){
+            if (result != null) {
                 when (result.status) {
                     Result.Status.LOADING -> {
                         binding?.progressBar?.visible(true)
@@ -167,7 +163,7 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
         })
 
         viewModel.sharePerson.observe(viewLifecycleOwner, Observer { result ->
-            if(result!=null){
+            if (result != null) {
                 when (result.status) {
                     Result.Status.LOADING -> {
                         binding?.progressBar?.visible(true)
@@ -195,7 +191,7 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
         }
         val deleteMenuItem = menu.findItem(R.id.deletePerson)
         val shareMenu = menu.findItem(R.id.sharePersonToUser)
-        val current_id = arguments?.getLong("current_id",-1)?:-1
+        val current_id = arguments?.getLong("current_id", -1)?:-1
         deleteMenuItem.isVisible = current_id>0
         shareMenu.isVisible = current_id>0
         return super.onCreateOptionsMenu(menu, inflater)
@@ -208,13 +204,12 @@ class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
             showError(getString(R.string.network_inactive))
             return false
         }
-        val current_id = arguments?.getLong("current_id",-1)?:-1
+        val current_id = arguments?.getLong("current_id", -1)?:-1
         when(item.itemId){
             R.id.addPerson -> {
-                if(current_id>0){
+                if (current_id > 0) {
                     viewModel.editPerson(current_id)
-                }
-                else {
+                } else {
                     viewModel.addPerson()
                 }
             }

+ 8 - 6
feature_person/src/main/java/com/mrozon/feature_person/presentation/EditPersonFragmentViewModel.kt

@@ -21,9 +21,11 @@ import javax.inject.Inject
 
 class EditPersonFragmentViewModel @Inject constructor(val context: Context, val repository: PersonRepository): BaseViewModel() {
 
-    private val _male = MutableLiveData<Boolean>(true)
-    val male: LiveData<Boolean>
-        get() = _male
+//    private val _male = MutableLiveData<Boolean>(true)
+//    val male: LiveData<Boolean>
+//        get() = _male
+
+    private var _male:Boolean = true
 
     private var _person = MutableLiveData<Result<Person>?>(null)
     val person: LiveData<Result<Person>?>
@@ -70,13 +72,13 @@ class EditPersonFragmentViewModel @Inject constructor(val context: Context, val
     }
 
     fun setMaleGender(isMale: Boolean) {
-        _male.value = isMale
+        _male = isMale
     }
 
     @ExperimentalCoroutinesApi
     fun addPerson() {
         var gender = Gender.MALE
-        if(_male.value==false)
+        if(_male==false)
             gender = Gender.FEMALE
         val personEntity = Person(name = personNameChannel.value, gender = gender, born = personDobChannel.value )
         viewModelScope.launch(Dispatchers.IO) {
@@ -115,7 +117,7 @@ class EditPersonFragmentViewModel @Inject constructor(val context: Context, val
     @ExperimentalCoroutinesApi
     fun editPerson(id: Long) {
         var gender = Gender.MALE
-        if(_male.value==false)
+        if(!_male)
             gender = Gender.FEMALE
         val personEntity = Person(id = id, name = personNameChannel.value, gender = gender, born = personDobChannel.value )
         viewModelScope.launch(Dispatchers.IO) {

+ 17 - 40
feature_person/src/main/res/layout/fragment_edit_person.xml

@@ -26,52 +26,17 @@
                 android:id="@+id/etPersonName"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:layout_marginStart="8dp"
-                android:layout_marginEnd="16dp"
+                android:layout_marginStart="32dp"
+                android:layout_marginEnd="32dp"
                 android:ems="10"
                 android:hint="@string/hint_person_name"
                 android:imeOptions="actionDone"
                 android:inputType="textPersonName"
                 app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toEndOf="@+id/ivGenger"
+                app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toTopOf="@id/guideline"
                 tools:text="Ivan Petrov" />
 
-            <ImageView
-                android:id="@+id/ivGenger"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginStart="16dp"
-                app:layout_constraintBottom_toBottomOf="@+id/etPersonName"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="@+id/etPersonName"
-                app:srcCompat="@drawable/ic_male_avatar" />
-
-            <RadioGroup
-                android:id="@+id/radioGroup"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="16dp"
-                android:checkedButton="@id/rbMale"
-                android:orientation="horizontal"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/etPersonName">
-
-                <RadioButton
-                    android:id="@+id/rbMale"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/male" />
-
-                <RadioButton
-                    android:id="@+id/rbFemale"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/female" />
-
-            </RadioGroup>
-
             <DatePicker
                 android:id="@+id/pickerBoD"
                 android:layout_width="wrap_content"
@@ -88,8 +53,8 @@
                 android:layout_height="wrap_content"
                 android:layout_marginTop="16dp"
                 android:text="@string/tvDOB"
-                app:layout_constraintStart_toStartOf="@+id/etPersonName"
-                app:layout_constraintTop_toBottomOf="@id/radioGroup" />
+                app:layout_constraintStart_toStartOf="@+id/pickerBoD"
+                app:layout_constraintTop_toBottomOf="@+id/genderSwitch" />
 
             <ProgressBar
                 android:id="@+id/progressBar"
@@ -104,6 +69,18 @@
                 app:layout_constraintTop_toTopOf="parent"
                 tools:visibility="visible" />
 
+            <com.mrozon.feature_person.customview.GenderSwitch
+                android:id="@+id/genderSwitch"
+                android:layout_width="0dp"
+                android:layout_height="50dp"
+                android:layout_marginStart="24dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="24dp"
+                android:padding="4dp"
+                app:layout_constraintEnd_toEndOf="@+id/pickerBoD"
+                app:layout_constraintStart_toStartOf="@+id/pickerBoD"
+                app:layout_constraintTop_toBottomOf="@+id/etPersonName" />
+
 
         </androidx.constraintlayout.widget.ConstraintLayout>
 

+ 7 - 0
feature_person/src/main/res/values/attrs.xml

@@ -0,0 +1,7 @@
+<resources>
+    <declare-styleable name="GenderSwitch">
+        <attr name="isMale" format="boolean" />
+        <attr name="colorChecked" format="color" />
+        <attr name="colorUnchecked" format="color" />
+    </declare-styleable>
+</resources>

+ 1 - 0
utils/build.gradle

@@ -42,6 +42,7 @@ dependencies {
     implementation navigationUi
     implementation timber
     implementation retrofit
+    implementation constraintlayout
     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
 
 

+ 6 - 31
utils/src/main/java/com/mrozon/utils/base/BaseFragment.kt

@@ -1,22 +1,14 @@
 package com.mrozon.utils.base
 
-import android.graphics.Color
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import android.view.ViewTreeObserver
-import android.view.InflateException
 import androidx.annotation.LayoutRes
-import androidx.core.content.ContextCompat.getColor
 import androidx.databinding.DataBindingUtil
 import androidx.databinding.ViewDataBinding
 import androidx.fragment.app.Fragment
-import androidx.navigation.Navigation.findNavController
-import androidx.navigation.fragment.NavHostFragment
 import com.google.android.material.snackbar.Snackbar
-import com.mrozon.utils.R
-import timber.log.Timber
 
 abstract class BaseFragment<T : ViewDataBinding>: Fragment()//,
 {
@@ -60,35 +52,18 @@ abstract class BaseFragment<T : ViewDataBinding>: Fragment()//,
     }
 
     fun showError(message: String, action:()->Unit) {
-        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_INDEFINITE)
-        snackbar.view.setBackgroundColor(getColor(requireContext(),R.color.color_snack_error))
-        snackbar.setActionTextColor(Color.WHITE)
-        snackbar.setAction(R.string.Ok) {
-            snackbar.dismiss()
-            action()
-        }
-        snackbar.show()
+        MyInfoDialog.newInstance(MyInfoDialogType.ERROR,message, action)
+            .show(requireActivity().supportFragmentManager, MyInfoDialog.TAG)
     }
 
     fun showError(message: String) {
-        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_INDEFINITE)
-        snackbar.view.setBackgroundColor(getColor(requireContext(),R.color.color_snack_error))
-        snackbar.setActionTextColor(Color.WHITE)
-        snackbar.setAction(R.string.Ok) {
-            snackbar.dismiss()
-        }
-        snackbar.show()
+        MyInfoDialog.newInstance(MyInfoDialogType.ERROR,message)
+            .show(requireActivity().supportFragmentManager, MyInfoDialog.TAG)
     }
 
     fun showInfo(message: String, action:()->Unit) {
-        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_INDEFINITE)
-        snackbar.view.setBackgroundColor(getColor(requireContext(),R.color.color_snack_info))
-        snackbar.setActionTextColor(Color.WHITE)
-        snackbar.setAction(R.string.Ok) {
-            snackbar.dismiss()
-            action()
-        }
-        snackbar.show()
+        MyInfoDialog.newInstance(MyInfoDialogType.INFO,message, action)
+            .show(requireActivity().supportFragmentManager, MyInfoDialog.TAG)
     }
 
     fun show(message: String) {

+ 100 - 0
utils/src/main/java/com/mrozon/utils/base/MyInfoDialog.kt

@@ -0,0 +1,100 @@
+package com.mrozon.utils.base
+
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.fragment.app.DialogFragment
+import com.mrozon.utils.R
+import kotlinx.android.synthetic.main.fragment_my_info_dialog.*
+
+class MyInfoDialog: DialogFragment() {
+
+    companion object {
+
+        private var action: () -> Unit = {}
+        const val TAG = "MyInfoDialog"
+
+        private const val KEY_TYPE = "KEY_TYPE"
+        private const val KEY_MESSAGE = "KEY_MESSAGE"
+
+        fun newInstance(type: MyInfoDialogType, message: String, action:()->Unit): MyInfoDialog {
+            val args = Bundle()
+            args.putInt(KEY_TYPE, type.ordinal)
+            args.putString(KEY_MESSAGE, message)
+            this.action = action
+            val fragment = MyInfoDialog()
+            fragment.arguments = args
+            return fragment
+        }
+
+        fun newInstance(type: MyInfoDialogType, message: String): MyInfoDialog {
+            val args = Bundle()
+            args.putInt(KEY_TYPE, type.ordinal)
+            args.putString(KEY_MESSAGE, message)
+            val fragment = MyInfoDialog()
+            fragment.arguments = args
+            return fragment
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_my_info_dialog, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        setupView()
+        setupClickListeners()
+    }
+
+    override fun onStart() {
+        super.onStart()
+        dialog?.window?.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        dialog?.setCanceledOnTouchOutside(false)
+        val decorView = dialog?.window?.decorView
+        val scaleDown: ObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(
+            decorView,
+            PropertyValuesHolder.ofFloat("scaleX", 0.0f, 1.0f),
+            PropertyValuesHolder.ofFloat("scaleY", 0.0f, 1.0f),
+            PropertyValuesHolder.ofFloat("alpha", 0.0f, 1.0f)
+        )
+        scaleDown.duration = 400
+        scaleDown.start()
+    }
+
+    private fun setupClickListeners() {
+        buttonCloseDialog.setOnClickListener {
+            dismiss()
+            action()
+        }
+    }
+
+    private fun setupView() {
+        val type = MyInfoDialogType.values()[arguments?.getInt(KEY_TYPE,0)?:0]
+        if(type==MyInfoDialogType.INFO){
+            ivTypeIcon.setImageResource(R.drawable.ic_success)
+        }
+        else
+        {
+            ivTypeIcon.setImageResource(R.drawable.ic_error)
+        }
+        tvMessage.text = arguments?.getString(KEY_MESSAGE,"")
+    }
+
+}
+
+enum class MyInfoDialogType {
+    INFO,
+    ERROR
+}

+ 8 - 0
utils/src/main/java/com/mrozon/utils/extension/ViewExt.kt

@@ -1,9 +1,12 @@
 package com.mrozon.utils.extension
 
 import android.view.View
+import android.view.animation.AnimationUtils
+import android.widget.Button
 import android.widget.DatePicker
 import android.widget.EditText
 import androidx.core.widget.doOnTextChanged
+import com.mrozon.utils.R
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.ConflatedBroadcastChannel
 import java.util.*
@@ -31,4 +34,9 @@ fun DatePicker.getDate(): Date {
     val calendar = Calendar.getInstance()
     calendar.set(year, month, dayOfMonth)
     return calendar.time
+}
+
+fun Button.shake() {
+    val shake = AnimationUtils.loadAnimation(this.context, R.anim.shake)
+    this.startAnimation(shake)
 }

+ 19 - 0
utils/src/main/res/anim/shake.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <rotate
+        android:duration="70"
+        android:fromDegrees="-5"
+        android:pivotX="50%"
+        android:pivotY="50%"
+        android:repeatCount="5"
+        android:repeatMode="reverse"
+        android:interpolator="@android:anim/linear_interpolator"
+        android:toDegrees="5" />
+    <translate
+        android:fromXDelta="-10"
+        android:toXDelta="10"
+        android:repeatCount="5"
+        android:repeatMode="reverse"
+        android:interpolator="@android:anim/linear_interpolator"
+        android:duration="70" />
+</set>

+ 6 - 0
utils/src/main/res/drawable/ic_error.xml

@@ -0,0 +1,6 @@
+<vector android:height="80dp" android:viewportHeight="455.111"
+    android:viewportWidth="455.111" android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#E24C4B" android:pathData="M227.556,227.556m-227.556,0a227.556,227.556 0,1 1,455.112 0a227.556,227.556 0,1 1,-455.112 0"/>
+    <path android:fillColor="#D1403F" android:pathData="M455.111,227.556c0,125.156 -102.4,227.556 -227.556,227.556c-72.533,0 -136.533,-32.711 -177.778,-85.333c38.4,31.289 88.178,49.778 142.222,49.778c125.156,0 227.556,-102.4 227.556,-227.556c0,-54.044 -18.489,-103.822 -49.778,-142.222C422.4,91.022 455.111,155.022 455.111,227.556z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M331.378,331.378c-8.533,8.533 -22.756,8.533 -31.289,0l-72.533,-72.533l-72.533,72.533c-8.533,8.533 -22.756,8.533 -31.289,0c-8.533,-8.533 -8.533,-22.756 0,-31.289l72.533,-72.533l-72.533,-72.533c-8.533,-8.533 -8.533,-22.756 0,-31.289c8.533,-8.533 22.756,-8.533 31.289,0l72.533,72.533l72.533,-72.533c8.533,-8.533 22.756,-8.533 31.289,0c8.533,8.533 8.533,22.756 0,31.289l-72.533,72.533l72.533,72.533C339.911,308.622 339.911,322.844 331.378,331.378z"/>
+</vector>

+ 7 - 0
utils/src/main/res/drawable/ic_success.xml

@@ -0,0 +1,7 @@
+<vector android:height="80dp" android:viewportHeight="455.431"
+    android:viewportWidth="455.431" android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#8DC640" android:pathData="M405.493,412.764c-69.689,56.889 -287.289,56.889 -355.556,0c-69.689,-56.889 -62.578,-300.089 0,-364.089s292.978,-64 355.556,0S475.182,355.876 405.493,412.764z"/>
+    <path android:fillAlpha="0.2" android:fillColor="#FFFFFF"
+        android:pathData="M229.138,313.209c-62.578,49.778 -132.267,75.378 -197.689,76.8c-48.356,-82.489 -38.4,-283.022 18.489,-341.333c51.2,-52.622 211.911,-62.578 304.356,-29.867C377.049,112.676 330.116,232.142 229.138,313.209z" android:strokeAlpha="0.2"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M195.004,354.453c-9.956,0 -19.911,-4.267 -25.6,-12.8l-79.644,-102.4c-11.378,-14.222 -8.533,-34.133 5.689,-45.511s34.133,-8.533 45.511,5.689l54.044,69.689l119.467,-155.022c11.378,-14.222 31.289,-17.067 45.511,-5.689s17.067,31.289 5.689,45.511L220.604,341.653C213.493,348.764 204.96,354.453 195.004,354.453z"/>
+</vector>

+ 44 - 0
utils/src/main/res/layout/fragment_my_info_dialog.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <ImageView
+        android:id="@+id/ivTypeIcon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/ic_success" />
+
+    <TextView
+        android:id="@+id/tvMessage"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="16dp"
+        android:layout_marginEnd="16dp"
+        android:textAlignment="center"
+        android:textSize="16sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.546"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/ivTypeIcon"
+        tools:text="adgjahsdgjahs" />
+
+    <Button
+        android:id="@+id/buttonCloseDialog"
+        style="@style/Widget.AppCompat.Button.Colored"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="16dp"
+        android:text="@string/button_close_dialog"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/tvMessage" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 1
utils/src/main/res/values/strings.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="Ok">Ok</string>
+    <string name="Ok">@android:string/ok</string>
     <string name="network_inactive">Sorry, no internet access!</string>
+    <string name="button_close_dialog">Close</string>
 </resources>