Сегодня первый большой снегопад в моем городе и у меня появилось хорошее настроение наскрябать небольшую статейку с некоторыми фишками из Android разработки.

Надеюсь, что моя статейка окажется полезной и ее не закидают плохими словами злые программисты.

Да именно злые, зло плодится, а вы не знали? Шучу конечно :)

Ну поехали :)

Получение цвета темы

Недавно я писал свою кастомную вьюху и мне нужно было установить цвет по умолчанию primaryColor и поэтому я создал небольшую Kotlin функцию, которая получает его из темы приложения:

// Kotlin расширение для получения цвета
fun Context.themeColor(@AttrRes attrRes: Int): Int {
	val typedValue = TypedValue()
  theme.resolveAttribute (attrRes, typedValue, true)
	return typedValue.data
}

// получаем primaryColor из темы прилы
context.themeColor(android.R.attr.colorPrimary)

Перевести dp в пиксели

Иногда нам нужно прописать оступ в коде и чтобы мы могли использовать независимые от плотности пиксели я довольно часто пишу следующее Kotlin расширения для своих кастомных вьюшках:

// Kotlin расширение, определенное внутри кастомной вьюшки
private fun Int.dp() = (context.resources.displayMetrics.density * this)
  .toInt()
  
// также можно реализовать через get
private fun Int.dp
	get() = (context.resources.displayMetrics.density * this).toInt()
                        
// устанавливаем padding в 10 dp
setPadding(10.dp(), 10.dp(), 10.dp(), 10.dp())                        

Создание круглого изображения

С этой проблемой я сталкивался ни один раз и поэтому решений довольно много, но я решил показать одно из самых неочевидных по моему мнению, создание кастомной вьюшки:

// отображает круглое изображение
class RoundedImageView @JvmOverloads constructor(
    ctx: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(ctx, attrs, defStyleAttr) {

    private val Int.dp
        get() = context.resources.displayMetrics.density * this

    private val roundedBitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = 0xff424242.toInt()
    }

    override fun onDraw(canvas: Canvas) {
      	// получаем наш bitmap
        val drawable = drawable ?: return
        if (width == 0 || height == 0) {
            return
        }
        val bitmap = (drawable as BitmapDrawable).bitmap
      	// делаем его круглым
        val roundedBitmap = rounded(bitmap, 100.dp)
        // и отрисовываем
        canvas.drawBitmap(roundedBitmap, 0f, 0f, null)
    }

    // cоздаем круглую bitmap'у
    private fun rounded(bitmap: Bitmap, radius: Float) : Bitmap {
        val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(result)
        canvas.drawARGB(0, 0, 0, 0)

        val rectF = RectF(0f, 0f, bitmap.width / 1f, bitmap.height / 1f)
        canvas.drawRoundRect(rectF,  radius, radius, roundedBitmapPaint)
        roundedBitmapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)

        val rect = Rect(0, 0, bitmap.width, bitmap.height)
        canvas.drawBitmap(bitmap, rect, rect, roundedBitmapPaint)

        return result
    }


}

Использование:

  <ru.freeit.personalapp.RoundedImageView
        android:id="@+id/avatar_img"
        android:layout_width="150dp"
        android:layout_height="150dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:src="@drawable/pony"
        android:scaleType="centerCrop" />

Анимация в кастомных вьюшках

В основном, я использую ValueAnimator:

 override fun onTouchEvent(event: MotionEvent): Boolean {
        return when (event.action) {
            MotionEvent.ACTION_DOWN -> {
              	// запускаем анимацию, когда произошло прикосновение к экрану
                val animator = ValueAnimator.ofFloat(0f, width.toFloat())
                animator.addUpdateListener {
                  	// обновляем ширину и перерисовываем View
                    animWidth = it.animatedValue as Float
                    invalidate()
                }
                // задержка анимации
                animator.duration = 400L
                animator.doOnEnd {
                  	// возвращает к исходному состоянию
                    animWidth = 0f
                    invalidate()
                }
                animator.start()
                true
            }
            MotionEvent.ACTION_UP -> {
                listener.firstOrNull()?.invoke()
                true
            }
            else -> super.onTouchEvent(event)
        }
    }

    override fun dispatchDraw(canvas: Canvas) {
        canvas.drawRect(8f, 8f, width - 8f, height - 8f, borderPaint)
        // значение animWidth меняется с каждым кадром анимации
        canvas.drawRect(0f, 0f, animWidth, height.toFloat(), bgPaint)
        super.dispatchDraw(canvas)
    }

Здесь я привел простенький пример без отмены предыдущей анимации.

Небольшая оберточка для SharedPreferences

В одном из приложений для простоты использования я реализовал простенькую обертку вокруг SharedPreferences и до сих пор ее юзаю:

// сохранение и чтение Int значений
interface IntPrefs {
    fun saveInt(ket: String, value: Int)
    fun int(key: String, default: Int) : Int
}

// сохранение и чтение String значений
interface StringPrefs {
    fun saveStr(key: String, value: String)
    fun str(key: String, default: String) : String
}

// обертка вокруг SharedPreferences
class LocalPrefsDataSource(ctx: Context) : IntPrefs, StringPrefs {

    private val prefsName = "app_prefs"
    private val sharedPrefs = ctx.getSharedPreferences(prefsName, Context.MODE_PRIVATE)

    override fun saveInt(key: String, value: Int) {
        sharedPrefs.edit().putInt(key, value).apply()
    }

    override fun saveStr(key: String, value: String) {
        sharedPrefs.edit().putString(key, value).apply()
    }

    override fun str(key: String, default: String) : String {
        return sharedPrefs.getString(key, default) ?: default
    }

    override fun int(key: String, default: Int) : Int {
        return sharedPrefs.getInt(key, default)
    }

}

Добавление навигации по кнопки назад в WebView

Если вы юзали WebView то вы знаете, что по умолчанию по нажатию на кнопку назад мы просто выйдем из приложения (если конечно backstack состоит только из одной активити или одного фрагмента).

Для организации навигации в WebView есть простое решение:

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        webView = findViewById(R.id.ali_express_web_view)
        val progressPageLoading = findViewById<CircularProgressIndicator>(R.id.progress_page_loading)
        webView.webViewClient = object: WebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                progressPageLoading.visibility = View.GONE
            }
        }
        webView.settings.javaScriptEnabled = true
      	// грузим наш любимый AliExpress :)
        webView.loadUrl("https://best.aliexpress.ru/")
    }

    // переопределяем onKeyDown для корректной навигации по сайтам в WebView
    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (event?.action == KeyEvent.ACTION_DOWN) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                if (webView.canGoBack()) {
                    webView.goBack()
                } else {
                    finish()
                }
                return true
            }
        }
        return super.onKeyDown(keyCode, event)
    }
}

Создание кастомного фрагмента для Google Maps

В документации по Google Maps рекомендуется использовать предопределенный фрагмент SupportMapFragment.

Но бывают ситуации, когда нам нужно кастомизировать View фрагмента, добавить поверх карты какие-либо элементы и поэтому нужно реализовать свой GoogleMapFragment:

class GoogleMapFragment : Fragment() {

  	private var mapView : MapView? = null
    private var googleMap: GoogleMap? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val binding = GoogleMapFragmentBinding.inflate(inflater, container, false)
        this.mapView = binding.mapView

      	// MapView имеет методы жизненного цикла которые нужно вызывать
        binding.mapView.onCreate(savedInstanceState)

        binding.mapView.getMapAsync { googleMap ->
          	// можем юзать Google Maps API
            this.googleMap = googleMap
        }

        return binding.root
    }
    
    override fun onStart() {
        super.onStart()
        mapView?.onStart()
    }

    override fun onResume() {
        super.onResume()
        mapView?.onResume()
    }

    override fun onPause() {
        super.onPause()
        mapView?.onPause()
    }

    override fun onStop() {
        super.onStop()
        mapView?.onStop()
    }

    override fun onDestroy() {
        super.onDestroy()
        mapView?.onDestroy()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView?.onSaveInstanceState(outState)
    }

    override fun onLowMemory() {
        super.onLowMemory()
        mapView?.onLowMemory()
    }

}

Разметка нашего фрагмента (google_map_fragment.layout):

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.google.android.gms.maps.MapView
        android:id="@+id/map_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- мы можем добавить что-нибудь поверх карты  -->

</FrameLayout>

Заключение

Ну и напоследок я хотел бы поделиться некоторыми моими репозиторчиками:

  • Kotlin-Algorithms-and-Design-Patterns  - я почти каждый день добавляю новые алгоритмы, структуры данных и паттерны проектирования.

  • LearningApps - репозиторий с мини приложениями, в каждом из которых проработана одна или несколько тем по Android разработке.

Как сказал, один из индийских разработчиков, изучайте и делитесь знаниями!!!

Комментарии (8)


  1. anegin
    02.11.2021 17:02

    Создание круглого изображения

    • создавать объекты в onDraw() - bad practice, о чем даже студия подсказывает

    • canvas.clipPath() с закруглением будет без anti-alias, будет видна "лесенка" на закруглениях


    1. KiberneticWorm Автор
      02.11.2021 17:12

      Спасибо, 1 пункт исправил, что касается "лесенки" на закруглениях то не совсем понимаю как это можно пофиксить, не могли бы вы подсказать?


      1. anegin
        02.11.2021 17:22
        +2

        Нужно вызывать path.reset() перед тем как заново его сформировать.

        Чтобы было без "лесенки" нужно использовать другой подход, используя PorterDuffXferMode - сначала рисуется исходная картинка, затем сверху рисуется нужная маска с нужным режимом.


        1. ookami_kb
          03.11.2021 01:39

          Или взять RoundedBitmapDrawable


        1. KiberneticWorm Автор
          03.11.2021 07:30

          Спасибо, уже изменил подход создания кругляшки)


  1. anegin
    02.11.2021 17:26
    +2

    Вместо fun Int.dp() можно сделать

    val Int.dp: Float
        get() = ...

    Тогда можно будет без скобок писать 10.dp

    А если dimen-ресурсы в проекте не зависят от конфигурации (поворот, локаль и т.п.), то можно вообще отвязаться от Context

    val Int.dp: Float
        get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics)


    1. KiberneticWorm Автор
      03.11.2021 07:29

      Спасибо, дополнил


  1. w201
    03.11.2021 06:54
    +2

    Ну и круглое изображение создавать через custom view уже довольно давно не актуально ShapeableImageView вам поможет.

    Почти все подобные советы устаревают за пол года :)