Weathyandroid

웨디의 안드로이드 저장소 ⭐️ 힘차게 가보으자 🚀
Alternatives To Weathyandroid
Project NameStarsDownloadsRepos Using ThisPackages Using ThisMost Recent CommitTotal ReleasesLatest ReleaseOpen IssuesLicenseLanguage
Android Best Practices19,837
2 years ago30other
Do's and Don'ts for Android development, by Futurice developers
Androiddevtools7,581
13 hours ago8
收集整理Android开发所需的Android SDK、开发中用到的工具、Android开发教程、Android设计规范,免费的设计素材等。
Shadow6,926
2 months ago253bsd-3-clauseJava
零反射全动态Android插件框架
Weiciyuan2,664
7 years ago63gpl-3.0Java
Sina Weibo Android Client
Androidprocess2,640
4 years ago15apache-2.0Java
判断App位于前台或者后台的6种方法
Android Samples2,338
10 days ago7apache-2.0Java
Samples demonstrating how to use Maps SDK for Android
Condom2,321
4 years agoapache-2.0Java
一个超轻超薄的Android工具库,阻止三方SDK中常见的有害行为,而不影响应用自身的功能。(例如严重影响用户体验的『链式唤醒』)
Open Keychain1,882
2 months ago428gpl-3.0Java
OpenKeychain is an OpenPGP implementation for Android.
Notifyutil1,631
3 years ago13apache-2.0Java
通知工具类
Weibo1,548
9 months ago12Java
第三方新浪微博客户端
Alternatives To Weathyandroid
Select To Compare


Alternative Project Comparisons
Readme

WeathyAndroid

All Contributors


dingding-21


kmebin


MJ Studio

: , , , , ,
: , , , Dialog, ,
: , , , ,

  • :
  • :
  • :
  • : ,
  • :
  • :
  • :
  • :
  • :
  • :

Gradle

  • SDK : 30
  • : 29.0.3
  • SDK : 23
  • SDK : 30
    • : true
    • : true
release {
    signingConfig signingConfigs.release
    shrinkResources true
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
    • : 1.8
    • desugar: true
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    coreLibraryDesugaringEnabled true
}
    • JVM : 1.8
kotlinOptions {
    jvmTarget = '1.8'
}
    • : true
    • : true
buildFeatures {
    dataBinding true
    viewBinding true
}
    • (Reboletrics)
testOptions {
    unitTests.returnDefaultValues = true
    unitTests {
        includeAndroidResources = true
    }
}
    • abortOnError
lintOptions {
    abortOnError false
}

  • [Preferences] - [Editor] - [Code Style] - [Kotlin] Kotlin style guide Reformat with code

git

main(default)

  • main default
  • main push
  • force push Github branch protection

(feature)

  1.    (`origin`)  `push`
    
  2. Pull Request
  3. Pull Request Create merge commit
  4. Pull Request (main) main pull

Github action & Slack Bot

  • push , apk
name: Android Build
on: [push]
defaults:
  run:
    shell: bash
    working-directory: .

jobs:
  build:
    runs-on: ubuntu-latest
    name: InstrumentationTest + Build
    if: "!contains(toJSON(github.event.commits.*.message), '[skip action]') && !startsWith(github.ref, 'refs/tags/')"
    steps:
      - name: Checkout repository
        uses: actions/[email protected]
      - name: Gradle cache
        uses: actions/[email protected]
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      #      - uses: actions/[email protected]
      #        with:
      #          java-version: '1.8'

      #      - name: Android Emulator Runner, Test
      #        uses: ReactiveCircus/[email protected]
      #        with:
      #          api-level: 29
      #          script: ./gradlew connectedCheck

      - name: Build Release
        if: ${{ contains(github.ref, 'main') }}
        run: ./gradlew assembleRelease
      - name: Build Debug
        if: ${{ !contains(github.ref, 'main') }}
        run: ./gradlew assembleDebug

      - name: Archive artifacts
        uses: actions/[email protected]
        with:
          path: app/build/outputs
      - name: Update Release apk name
        if: ${{ success() && contains(github.ref, 'main') }}
        run: |
          mv app/build/outputs/apk/release/app-release.apk -.apk
          echo 'apk=-.apk' >> $GITHUB_ENV
      - name: Upload APK
        if: ${{ success() && contains(github.ref, 'main') }}
        run: |
          curl -X POST \
          -F [email protected]$apk \
          -F channels=${{secrets.SLACK_CHANNEL_ID}} \
          -H "Authorization: Bearer ${{secrets.SLACK_BOT_API_TOKEN}}" \
          https://slack.com/api/files.upload
      - name: On success, Notify with Slack
        if: ${{ success() && contains(github.ref, 'main') }}
        uses: rtCamp/[email protected]
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_ANDROID }}
          SLACK_TITLE: '    '
          MSG_MINIMAL: true
          SLACK_MESSAGE: 'apk '
      - name: On fail, Notify with Slack
        if: ${{ failure() }}
        uses: rtCamp/[email protected]
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_ANDROID }}
          SLACK_TITLE: '   '
          MSG_MINIMAL: false
          SLACK_MESSAGE: ' '

Instrumentation Test

  • Glide: url ImageView .
  • Retrofit: OkHttp3 Rest API .
  • Material Design Component: . UI
  • SwipeRefreshLayout: .
  • AAC Lifecycle: LiveData, Lifecycle, ViewModel
  • Kotlin Standard Library:
  • Jetpack Activity: Activity
  • Jetpack Fragment: Fragment
  • Jetpack Core KTX
  • ConstraintLayout: ConstraintLayout . MotionLayout
  • Dexter: .
  • Google Mobile Service Location:
  • Dynamic animation: Spring
  • Desugar JDK Library: java.time desugaring
  • Flipper:
  • Hilt:
  • Room: ,
  • LoremIpsum: Mock
  • Lottie:
  • Snowfall : ,

  • AAC DataBinding, ViewModel
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val viewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        binding.vm = viewModel
        binding.lifecycleOwner = this
        setContentView(binding.root)
        ...
    }
    • Coroutine
override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    initialJob = GlobalScope.launch(Dispatchers.Main) {
        ...
    }
}

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    initialJob.cancel()
}
    • java.time.LocalDate, java.util.Calendar
  • Java 8 desugaring
fun convertMonthlyIndexToDateToFirstDateOfMonthCalendar(index: Int): Pair<LocalDate, LocalDate> {
    val cur = LocalDate.now()

    val diffMonth = MonthlyAdapter.MAX_ITEM_COUNT - index - 1
    val monthSubtracted = cur.minusMonths(diffMonth.toLong())
    val firstDateOfMonth = monthSubtracted.withDayOfMonth(1)
    val startIdx = firstDateOfMonth.dayOfWeekIndex

    return firstDateOfMonth.minusDays(startIdx.toLong()) to firstDateOfMonth
}

( )

TODO

  • (LocationService, FusedLocationProviderClient, Geocoder)

LocationManager SharedPreferences .

LocationUtil.kt

@SuppressLint("MissingPermission")
class LocationUtil @Inject constructor(app: Application, private val spUtil: SPUtil) : DefaultLifecycleObserver {
    private val locationManager = app.getSystemService(LocationManager::class.java)

    private val _lastLocation = MutableStateFlow<Location?>(null)
    val lastLocation: StateFlow<Location?> = _lastLocation

    private val _isOtherPlaceSelected: MutableStateFlow<Boolean> = MutableStateFlow(false)
    val isOtherPlaceSelected: StateFlow<Boolean> = _isOtherPlaceSelected

    val selectedWeatherLocation: MutableStateFlow<OverviewWeather?> = MutableStateFlow(null)

    private var isRegistered = false

    override fun onCreate(owner: LifecycleOwner) {
        registerLocationListener()

        _isOtherPlaceSelected.value = spUtil.isOtherPlaceSelected
    }

    override fun onDestroy(owner: LifecycleOwner) {
        unregisterLocationListener()
    }

    private val locationListener = object : LocationListener {
        override fun onLocationChanged(location: Location) {
            _lastLocation.value = location
        }

        override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
        }

        override fun onProviderEnabled(provider: String) {
        }

        override fun onProviderDisabled(provider: String) {
        }
    }

    fun registerLocationListener() {
        if (isRegistered) return

        debugE("registerLocationListener")
        try {
            _lastLocation.value = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)

            val enabledProviders = locationManager.allProviders.filter {
                locationManager.isProviderEnabled(it)
            }
            val provider =
                if (LocationManager.GPS_PROVIDER in enabledProviders) LocationManager.GPS_PROVIDER else enabledProviders.first()

            locationManager.requestLocationUpdates(provider, 1000, 1f, locationListener)
            isRegistered = true
        } catch (e: Throwable) {
            debugE(e)
        }
    }

    private fun unregisterLocationListener() {
        debugE("unregisterLocationListener")

        locationManager.removeUpdates(locationListener)
        isRegistered = false
    }

    fun selectPlace(weather: OverviewWeather) {
        spUtil.lastSelectedLocationCode = weather.region.code
        selectedWeatherLocation.value = weather
        spUtil.isOtherPlaceSelected = false
        _isOtherPlaceSelected.value = false
    }

    fun selectOtherPlace(weather: OverviewWeather) {
        spUtil.lastSelectedLocationCode = weather.region.code
        selectedWeatherLocation.value = weather
        spUtil.isOtherPlaceSelected = true
        _isOtherPlaceSelected.value = true
    }
}
  • (WeathyCardView)

MaterialShapeDrawable ShapeAppearanceModel MDC .

WeathyCardView.kt

class WeathyCardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    FrameLayout(context, attrs) {
    private val defaultShadowColor = Color.BLACK

    var radius by OnChangeProp(35.dpFloat) {
        updateUI()
    }
    var shadowColor by OnChangeProp(defaultShadowColor) {
        updateUI()
    }
    var disableShadow by OnChangeProp(false) {
        updateUI()
    }
    var strokeColor by OnChangeProp(Color.TRANSPARENT) {
        updateUI()
    }
    var strokeWidth by OnChangeProp(0f) {
        updateUI()
    }
    var cardBackgroundColor by OnChangeProp(Color.WHITE) {
        updateUI()
    }

    init {
        if (attrs != null) {
            getStyleableAttrs(attrs)
        }
        elevation = if (disableShadow) 0f else px(8).toFloat()
        updateUI()
    }

    private fun getStyleableAttrs(attr: AttributeSet) {
        context.theme.obtainStyledAttributes(attr, R.styleable.WeathyCardView, 0, 0).use { arr ->
            radius = arr.getDimension(R.styleable.WeathyCardView_weathy_radius, 35.dpFloat)
            shadowColor = arr.getColor(R.styleable.WeathyCardView_weathy_shadow_color, defaultShadowColor)
            disableShadow = arr.getBoolean(R.styleable.WeathyCardView_weathy_disable_shadow, false)
            strokeColor = arr.getColor(R.styleable.WeathyCardView_weathy_stroke_color, Color.TRANSPARENT)
            strokeWidth = arr.getDimension(R.styleable.WeathyCardView_weathy_stroke_width, 0f)
            cardBackgroundColor = arr.getColor(R.styleable.WeathyCardView_weathy_background_color, Color.WHITE)
        }
    }

    private fun updateUI() {
        background = MaterialShapeDrawable(ShapeAppearanceModel().withCornerSize(radius)).apply {
            fillColor = ColorStateList.valueOf(cardBackgroundColor)
            strokeWidth = [email protected]
            strokeColor = ColorStateList.valueOf([email protected])
        }
        setShadowColorIfAvailable(shadowColor)
    }
}
  • (CalendarView)
 .

, . , index index , two-way binding . .

java.time gradle desugaring LocalDate, LocalDateTime .

<team.weathy.view.calendar.CalendarView
    android:id="@+id/calendarView"
    android:layout_width="match_parent"
    android:layout_height="220dp"
    app:layout_constraintTop_toTopOf="parent" />

CalendarView.kt

class CalendarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    ConstraintLayout(context, attrs) {
    private val today = LocalDate.now()

    var onDateChangeListener: ((date: LocalDate) -> Unit)? = null
    var onSelectedDateChangeListener: ((date: LocalDate) -> Unit)? = null

    private val curDateLiveData = MutableLiveData(LocalDate.now())
    var curDate: LocalDate by OnChangeProp(LocalDate.now()) {
         onCurDateChanged()
    }

    private val selectedDateLiveData = MutableLiveData(LocalDate.now())
    var selectedDate: LocalDate by OnChangeProp(LocalDate.now()) {
        selectedDateLiveData.value = it
        onSelectedDateChangeListener?.invoke(it)
        curDate = it
        invalidate()
    }
    private var rowCount = 4

    private val dataLiveData = MutableLiveData<Map<YearMonthFormat, List<CalendarPreview?>>>(mapOf())
    var data: Map<YearMonthFormat, List<CalendarPreview?>> by OnChangeProp(mapOf()) {
        dataLiveData.value = it
    }

    var onClickYearMonthText: (() -> Unit)? = null

    private val isTodayInCurrentMonth
        get() = curDate.year == today.year && curDate.month == today.month
    private val isTodayInCurrentWeek
        get() = isTodayInCurrentMonth && curDate.weekOfMonth == today.weekOfMonth
    private val isSelectedInCurrentWeek
        get() = selectedDate.year == curDate.year && selectedDate.month == curDate.month && selectedDate.weekOfMonth == curDate.weekOfMonth


    private val animLiveData = MutableLiveData(0f)
    private var animValue by OnChangeProp(0f) {
        animLiveData.value = it
        onAnimValueChanged()
    }

    private val scrollEnabled = MutableLiveData(false)
    private val onScrollToTop = SimpleEventLiveData()

    private val collapsedHeight
        get() = px(MIN_HEIGHT_DP)
    private val expandedHeight
        get() = screenHeight - px(EXPAND_MARGIN_BOTTOM_DP)

    private val paddingHorizontal = px(24)

    private val yearMonthText = TextView(context).apply {
        id = ViewCompat.generateViewId()
        setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25f)
        if (!isInEditMode) typeface = ResourcesCompat.getFont(context, R.font.roboto_medium)
        setTextColor(getColor(R.color.main_grey))
        gravity = Gravity.CENTER
        stateListAnimator = AnimatorInflater.loadStateListAnimator(context, R.animator.pressed_alpha_state_list_anim)

        layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
            topToTop = parentId
            leftToLeft = parentId
            rightToRight = parentId
            topMargin = px(26)
        }
        setOnDebounceClickListener {
            onClickYearMonthText?.invoke()
        }
    }

    private val downArrow = ImageView(context).apply {
        id = ViewCompat.generateViewId()
        setImageResource(R.drawable.calendar_btn_arrow)
        scaleType = FIT_CENTER
        layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
            topToTop = yearMonthText.id
            bottomToBottom = yearMonthText.id
            leftToRight = yearMonthText.id
            leftMargin = 4.dp
        }
    }

    private val todayButton = ImageButton(context).apply {
        setImageResource(R.drawable.ic_today)
        scaleType = FIT_CENTER

        val outValue = TypedValue()
        context.theme.resolveAttribute(attr.selectableItemBackgroundBorderless, outValue, true)
        setBackgroundResource(outValue.resourceId)

        setOnDebounceClickListener {
            curDate = today
            selectedDate = today
        }

        layoutParams = LayoutParams(px(32), px(32)).apply {
            setPadding(px(6), px(6), px(6), px(6))
            topToTop = yearMonthText.id
            bottomToBottom = yearMonthText.id
            rightToRight = parentId
            rightMargin = px(0)
        }
    }

    private val topDivider = View(context).apply {
        id = ViewCompat.generateViewId()
        setBackgroundColor(getColor(R.color.sub_grey_5))

        layoutParams = LayoutParams(MATCH_PARENT, px(1)).apply {
            topToBottom = yearMonthText.id
            topMargin = px(11)
        }
    }

    private val weekTextLayout = LinearLayout(context).apply {
        id = ViewCompat.generateViewId()
        orientation = LinearLayout.HORIZONTAL
        weightSum = 7f

        layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
            topToBottom = topDivider.id
            topMargin = px(16)
        }
    }
    private val weekTexts = (0..6).map {
        TextView(context).apply {
            id = ViewCompat.generateViewId()
            setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f)
            text = listOf("", "", "", "", "", "", "")[it]
            gravity = Gravity.CENTER

            layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, 1f)
        }
    }

    private var isExpanded = false
    private fun expand() {
        isExpanded = true
        notifyEnableScroll()
        enableTouchMonthlyPagerOnly()

        springAnim = AnimUtil.runSpringAnimation(animValue * 500f, 500f) {
            animValue = it / 500f
        }

        onExpandedChange()
    }

    private fun collapse() {
        isExpanded = false
        notifyDisableScroll()
        enableTouchWeeklyPagerOnly()

        springAnim = AnimUtil.runSpringAnimation(animValue * 500f, 0f) {
            animValue = it / 500f
        }

        onExpandedChange()
    }

    private fun onExpandedChange() {
        notifyScrollToTop()
        invalidate()
    }

    private val fragmentViewLifecycleOwner
        get() = findFragment<Fragment>().viewLifecycleOwner

    private val monthlyViewPagerGenerator = {
        ViewPager2(context).apply {
            layoutParams = LayoutParams(MATCH_PARENT, 0).apply {
                topToBottom = weekTextLayout.id
                bottomToBottom = parentId
                bottomMargin = px(32)
            }

            adapter = MonthlyAdapter(
                animLiveData,
                scrollEnabled,
                onScrollToTop,
                dataLiveData,
                selectedDateLiveData,
                fragmentViewLifecycleOwner,
            ) {
                if (!it.isFuture()) selectedDate = it
            }
            setCurrentItem(MonthlyAdapter.MAX_ITEM_COUNT, false)
            alpha = 0f

            setPageTransformer { page, position ->
                page.pivotX = if (position < 0) page.width.toFloat() else 0f
                page.pivotY = page.height * 0.5f
                page.rotationY = 25f * position
            }

            registerOnPageChangeCallback(object : OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    val (_, firstDateOfMonth) = convertMonthlyIndexToDateToFirstDateOfMonthCalendar(
                        position
                    )
                    if (isExpanded && curDate != firstDateOfMonth) {
                        curDate = firstDateOfMonth
                    }
                }
            })

            offscreenPageLimit = 1
        }
    }
    private var monthlyViewPager: ViewPager2? = null
    private val weeklyViewPagerGenerator = {
        ViewPager2(context).apply {
            layoutParams = LayoutParams(MATCH_PARENT, 0).apply {
                topToBottom = weekTextLayout.id
                height = px(WeeklyView.ITEM_HEIGHT_DP)
            }

            adapter = WeeklyAdapter(animLiveData, dataLiveData, fragmentViewLifecycleOwner) {
                if (!it.isFuture()) selectedDate = it
            }
            setCurrentItem(WeeklyAdapter.MAX_ITEM_COUNT, false)

            setPageTransformer { page, position ->
                page.pivotX = if (position < 0) page.width.toFloat() else 0f
                page.pivotY = page.height * 0.5f
                page.rotationY = 40f * position
            }

            registerOnPageChangeCallback(object : OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    val newDate = convertWeeklyIndexToFirstDateOfWeekCalendar(position)
                    if (!isExpanded && curDate != newDate) {
                        curDate = newDate
                    }
                }
            })

            offscreenPageLimit = 1
        }
    }
    private var weeklyViewPager: ViewPager2? = null

    init {
        initContainer()
        addViews()
        configureExpandGestureHandling()
        enableTouchWeeklyPagerOnly()
        changeWeekTextsColor()
        onCurDateChanged()
    }


    private val scope = CoroutineScope(Job() + Dispatchers.Main)
    private lateinit var lazyPagerAddJob: Job
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        lazyPagerAddJob = scope.launch {
            if (weeklyViewPager == null) {
                weeklyViewPager = weeklyViewPagerGenerator()
                TransitionManager.beginDelayedTransition([email protected])
                addView(weeklyViewPager!!, 0)
            }
            delay(400)
            if (monthlyViewPager == null) {
                monthlyViewPager = monthlyViewPagerGenerator()
                addView(monthlyViewPager!!, 0)
            }
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        lazyPagerAddJob.cancel()
    }

    private fun initContainer() {
        setPadding(paddingHorizontal, 0, paddingHorizontal, 0)

        background = MaterialShapeDrawable(
            ShapeAppearanceModel().toBuilder().setBottomLeftCorner(CornerFamily.ROUNDED, px(35).toFloat())
                .setBottomRightCorner(CornerFamily.ROUNDED, px(35).toFloat()).build()
        ).apply {
            fillColor = ColorStateList.valueOf(getColor(R.color.white))
        }
        elevation = px(4).toFloat()
    }

    private fun addViews() {
        addView(yearMonthText)
        addView(downArrow)
        addView(todayButton)
        addView(topDivider)
        addWeekLayoutAndWeekTexts()
    }

    private fun addWeekLayoutAndWeekTexts() = weekTextLayout.also { layout ->
        addView(layout)
        weekTexts.forEach(layout::addView)
    }

    private fun onCurDateChanged() {
        curDateLiveData.value = curDate
        rowCount = calculateRequiredRow(curDate)

        setYearMonthTextWithDate(curDate)
        selectPagerItemsWithDate(curDate)

        onDateChangeListener?.invoke(curDate)
        changeWeekTextsColor()
        notifyScrollToTop()
        invalidate()
    }

    private fun setYearMonthTextWithDate(date: LocalDate) {
        yearMonthText.text = "${date.year} .${date.monthValue}"
    }

    private fun selectPagerItemsWithDate(date: LocalDate) {
        val nextMonthlyIndex = convertDateToMonthlyIndex(date)
        val nextWeeklyIndex = convertDateToWeeklyIndex(date)

        if (monthlyViewPager?.currentItem != nextMonthlyIndex) {
            monthlyViewPager?.setCurrentItem(
                nextMonthlyIndex, false
            )
        }

        if (weeklyViewPager?.currentItem != nextWeeklyIndex) {
            weeklyViewPager?.setCurrentItem(
                nextWeeklyIndex, false
            )
        }
    }


    private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = getColor(R.color.main_mint)
    }
    private val greyCapsulePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = getColor(R.color.sub_grey_5)
        setShadowLayer(12f, 0f, 0f, getColor(R.color.sub_grey_5))
    }
    private val capsulePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = getColor(R.color.main_mint)
        setShadowLayer(12f, 0f, 0f, getColor(R.color.main_mint))
    }

    override fun onDraw(canvas: Canvas) {
        // bar
        canvas.drawRoundRect(
            width / 2f - px(30),
            height - pxFloat(16),
            width / 2f + px(30),
            height - pxFloat(11),
            pxFloat(10),
            pxFloat(10),
            barPaint,
        )

        // capsule
        val widthWithoutPadding = width - paddingHorizontal * 2f
        val rawWidth = widthWithoutPadding / 7f
        val maxWidth = pxFloat(42)
        val capsuleWidth = rawWidth.coerceAtMost(maxWidth)
        val capsuleLeftPadding = if (rawWidth >= maxWidth) (rawWidth - maxWidth) / 2f else 0f
        val capsuleHeight = pxFloat(64)
        val capsuleLeft = paddingHorizontal + capsuleLeftPadding + today.dayOfWeekIndex * rawWidth
        val capsuleWidthRadius = capsuleWidth / 2f
        val capsuleTop = pxFloat(72)

        val greyCapsuleLeft = paddingHorizontal + capsuleLeftPadding + selectedDate.dayOfWeekIndex * rawWidth
        if (isSelectedInCurrentWeek) {
            canvas.drawRoundRect(
                greyCapsuleLeft,
                capsuleTop,
                greyCapsuleLeft + capsuleWidth,
                capsuleTop + capsuleHeight,
                capsuleWidthRadius,
                capsuleWidthRadius,
                greyCapsulePaint
            )
        }

        if (isTodayInCurrentWeek) {
            canvas.drawRoundRect(
                capsuleLeft,
                capsuleTop,
                capsuleLeft + capsuleWidth,
                capsuleTop + capsuleHeight,
                capsuleWidthRadius,
                capsuleWidthRadius,
                capsulePaint,
            )
        }
    }

    private var springAnim: SpringAnimation? = null
    private var tracker: VelocityTracker? = null

    @SuppressLint("Recycle")
    private fun configureExpandGestureHandling() {
        setOnTouchListener { view, event ->
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    if (event.y <= view.height - px(30)) {
                        [email protected] false
                    }

                    springAnim?.cancel()

                    tracker?.clear()
                    tracker = tracker ?: VelocityTracker.obtain()
                    tracker?.addMovement(event)
                }
                MotionEvent.ACTION_MOVE -> {
                    tracker?.apply {
                        addMovement(event)
                        computeCurrentVelocity(1000)
                    }

                    animValue = ((event.y - collapsedHeight) / (expandedHeight - collapsedHeight)).clamp(0f, 1.2f)
                }
                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                    if (tracker!!.yVelocity > 0) expand()
                    else collapse()

                    tracker?.also {
                        it.recycle()
                        tracker = null
                    }
                }
            }
            true
        }
    }

    private fun onAnimValueChanged() {
        animateHeight()
        changeWeekTextsColor()
        animCapsulePaintAlpha()
        animPagersAlpha()
    }

    private fun animateHeight() = updateLayoutParams<ViewGroup.LayoutParams> {
        height = MathUtils.lerp(collapsedHeight.toFloat(), expandedHeight.toFloat(), animValue).toInt()
    }

    private fun changeWeekTextsColor() {
        weekTexts.forEachIndexed { idx, textView ->
            textView.setTextColor(
                CalendarUtil.getWeekTextColor(
                    context, idx, animValue, isTodayInCurrentWeek && today.dayOfWeekIndex == idx
                )
            )
        }
    }


    private fun animCapsulePaintAlpha() {
        capsulePaint.alpha = (255 - animValue * 255).toInt().clamp(0, 255)
        greyCapsulePaint.alpha = (255 - animValue * 255).toInt().clamp(0, 255)
    }

    private fun animPagersAlpha() {
        weeklyViewPager?.alpha = 1 - animValue
        monthlyViewPager?.alpha = animValue
    }

    private fun notifyDisableScroll() {
        scrollEnabled.value = false
    }

    private fun notifyEnableScroll() {
        scrollEnabled.value = true
    }

    private fun notifyScrollToTop() {
        onScrollToTop.emit()
    }

    private fun enableTouchWeeklyPagerOnly() {
        weeklyViewPager?.isUserInputEnabled = true
        monthlyViewPager?.isUserInputEnabled = false

        if (weeklyViewPager != null && monthlyViewPager != null) {
            removeViewAt(0)
            removeViewAt(0)
            addView(monthlyViewPager, 0)
            addView(weeklyViewPager, 1)
        }
    }

    private fun enableTouchMonthlyPagerOnly() {
        weeklyViewPager?.isUserInputEnabled = false
        monthlyViewPager?.isUserInputEnabled = true

        if (weeklyViewPager != null && monthlyViewPager != null) {
            removeViewAt(0)
            removeViewAt(0)
            addView(weeklyViewPager, 0)
            addView(monthlyViewPager, 1)
        }
    }

    companion object {
        private const val parentId = ConstraintSet.PARENT_ID
        private const val MIN_HEIGHT_DP = 224
        private const val EXPAND_MARGIN_BOTTOM_DP = 120
    }
}

DateTime.kt

fun convertMonthlyIndexToDateToFirstDateOfMonthCalendar(index: Int): Pair<LocalDate, LocalDate> {
    val cur = LocalDate.now()

    val diffMonth = MonthlyAdapter.MAX_ITEM_COUNT - index - 1
    val monthSubtracted = cur.minusMonths(diffMonth.toLong())
    val firstDateOfMonth = monthSubtracted.withDayOfMonth(1)
    val startIdx = firstDateOfMonth.dayOfWeekIndex

    return firstDateOfMonth.minusDays(startIdx.toLong()) to firstDateOfMonth
}

fun convertWeeklyIndexToFirstDateOfWeekCalendar(index: Int): LocalDate {
    val cur = LocalDate.now()

    val diffWeek = WeeklyAdapter.MAX_ITEM_COUNT - index - 1
    val weekSubtracted = cur.minusWeeks(diffWeek.toLong())
    val startIdx = weekSubtracted.dayOfWeekIndex

    return weekSubtracted.minusDays(startIdx.toLong())
}

fun convertDateToMonthlyIndex(date: LocalDate): Int {
    val now = LocalDate.now()

    val yearDiff = now.year - date.year
    val diffIndex = now.monthValue - date.monthValue + yearDiff * 12

    return MonthlyAdapter.MAX_ITEM_COUNT - diffIndex - 1
}

fun convertDateToWeeklyIndex(date: LocalDate): Int {
    val now = LocalDate.now()
    val nowDayOfWeekIndex = now.dayOfWeekIndex
    val nowFirstDayOfWeek = now.minusDays(nowDayOfWeekIndex.toLong())

    val dateDayOfWeekIndex = date.dayOfWeekIndex
    val dateFirstDayOfWeek = date.minusDays(dateDayOfWeekIndex.toLong())

    val weekDiff = dateFirstDayOfWeek.until(nowFirstDayOfWeek, ChronoUnit.WEEKS).toInt()

    return WeeklyAdapter.MAX_ITEM_COUNT - weekDiff - 1
}


fun calculateRequiredRow(date: LocalDate): Int {
    return (date.lengthOfMonth() + date.withDayOfMonth(1).dayOfWeekIndex - 1) / 7 + 1
}

fun getStartDateStringInCalendar(year: Int, month: Int): DateString {
    val date = LocalDate.of(year, month, 1)
    val startDayIndex = date.dayOfWeekIndex

    val startDateInCalendar = date.minusDays(startDayIndex.toLong())
    return startDateInCalendar.dateString
}

fun getEndDateStringInCalendar(year: Int, month: Int): DateString {
    val firstDate = LocalDate.of(year, month, 1)
    val lastDate = LocalDate.of(year, month, firstDate.lengthOfMonth())
    val endDayIndex = lastDate.dayOfWeekIndex

    val endDateInCalendar = lastDate.plusDays(6 - endDayIndex.toLong())
    return endDateInCalendar.dateString
}

Dexter .

PermissionUtil.kt

/**
 * Dexter       
 *
 * @author MJStudio
 * @see android.Manifest.permission
 */
object PermissionUtil {
    fun requestLocationPermissions(activity: Activity, listener: PermissionListener) {
        requestPermissions(
            activity, listOf(
                android.Manifest.permission.ACCESS_COARSE_LOCATION,
                android.Manifest.permission.ACCESS_FINE_LOCATION,
            ), listener
        )
    }

    /**
     *     
     *
     * @param activity Dexter   Activity 
     * @param listener        
     * @param permissions    [android.Manifest.permission]
     */
    private fun requestPermissions(activity: Activity, permissions: Collection<String>, listener: PermissionListener) {

        val callbackListener: MultiplePermissionsListener = object : BaseMultiplePermissionsListener() {

            override fun onPermissionsChecked(report: MultiplePermissionsReport) {

                val deniedPermissions = report.deniedPermissionResponses.map { it.permissionName }
                val permanentlyDeniedPermissions =
                    report.deniedPermissionResponses.filter { it.isPermanentlyDenied }.map { it.permissionName }

                //   ,
                when {
                    report.areAllPermissionsGranted() -> {
                        listener.onPermissionGranted()
                    }
                    //      
                    report.isAnyPermissionPermanentlyDenied -> {
                        listener.onAnyPermissionsPermanentlyDeined(deniedPermissions, permanentlyDeniedPermissions)
                    }
                    //     
                    else -> {
                        listener.onPermissionShouldBeGranted(deniedPermissions)
                    }
                }
            }

        }

        /**
         * Dexter activity   
         */
        Dexter.withActivity(activity).withPermissions(permissions).withListener(callbackListener).check()
    }


    fun openPermissionSettings(context: Context) {
        context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", context.packageName, null)
        })
    }

    interface PermissionListener {
        /**
         *   .
         */
        fun onPermissionGranted() {}

        /**
         *   .
         *
         * @param deniedPermissions   
         */
        fun onPermissionShouldBeGranted(deniedPermissions: List<String>) {}

        /**
         *    .
         *
         * @param deniedPermissions   
         * @param permanentDeniedPermissions    
         */
        fun onAnyPermissionsPermanentlyDeined(
            deniedPermissions: List<String>, permanentDeniedPermissions: List<String>
        ) {
        }
    }
}
  • StatusBar

Status bar . Window .

StatusBarUtil.kt

@Suppress("DEPRECATION")
object StatusBarUtil {
    fun changeColor(activity: Activity, @ColorInt color: Int) {
        activity.window?.run {
            statusBarColor = color
        }
    }
}
  • Dagger mock api module

    Qualifier @Api Api @ApiMock Mocking Api . .

ApiModule.kt

@Qualifier
@Retention(BINARY)
annotation class Api

@Qualifier
@Retention(BINARY)
annotation class ApiMock

@Module
@InstallIn(ApplicationComponent::class)
class ApiModule {
    @Provides
    @Singleton
    fun provideRetrofitProvider(uniqueId: UniqueIdentifier) = ApiFactory(uniqueId)

    @Provides
    @Singleton
    @Api
    fun provideAuth(provider: ApiFactory) = provider.createApi(AuthAPI::class)

    @Provides
    @Singleton
    @Api
    fun provideCalendar(provider: ApiFactory) = provider.createApi(CalendarAPI::class)

    @Provides
    @Singleton
    @Api
    fun provideClothes(provider: ApiFactory) = provider.createApi(ClothesAPI::class)

    @Provides
    @Singleton
    @Api
    fun provideUser(provider: ApiFactory) = provider.createApi(UserAPI::class)

    @Provides
    @Singleton
    @Api
    fun provideWeather(provider: ApiFactory) = provider.createApi(WeatherAPI::class)

    @Provides
    @Singleton
    @Api
    fun provideWeahy(provider: ApiFactory) = provider.createApi(WeathyAPI::class)
}

@Module
@InstallIn(ApplicationComponent::class)
abstract class ApiModuleMock {
    @Singleton
    @Binds
    @ApiMock
    abstract fun bindUser(api: MockUserAPI): UserAPI

    @Singleton
    @Binds
    @ApiMock
    abstract fun bindCalendar(api: MockCalendarAPI): CalendarAPI

    @Singleton
    @Binds
    @ApiMock
    abstract fun bindWeather(api: MockWeatherAPI): WeatherAPI

    @Singleton
    @Binds
    @ApiMock
    abstract fun bindWeathy(api: MockWeathyAPI): WeathyAPI
}
  • CommonDialog DialogFragment

DialogFragment Dialog . argument , Dialog .

@AndroidEntryPoint
class CommonDialog : DialogFragment() {
    private var binding by AutoClearedValue<DialogCommonBinding>()

    @Inject
    lateinit var pixelRatio: PixelRatio

    private val title: String
        get() = arguments?.getString("title") ?: ""
    private val body: String
        get() = arguments?.getString("body") ?: ""
    private val btnText: String
        get() = arguments?.getString("btnText") ?: ""
    private val color: Int
        get() = arguments?.getInt("color", getColor(R.color.blue_temp)) ?: getColor(R.color.blue_temp)
    private val showCancel: Boolean
        get() = arguments?.getBoolean("showCancel") ?: false
    private val clickListener: ClickListener?
        get() = if (parentFragment == null) (activity as? ClickListener) else (parentFragment as? ClickListener)

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
        DialogCommonBinding.inflate(inflater, container, false).also {
            binding = it
        }.root

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        isCancelable = false
        binding.title.text = title
        binding.body.text = body
        binding.btn.text = btnText
        binding.btn setOnDebounceClickListener {
            clickListener?.onClickYes()
            dismiss()
        }
        binding.title.setTextColor(color)
        binding.btn.backgroundTintList = ColorStateList.valueOf(color)

        if (showCancel) {
            binding.btn.updateLayoutParams<ConstraintLayout.LayoutParams> {
                leftMargin = 13.dp
            }
            binding.btnCancel.isVisible = true
            binding.btnCancel setOnDebounceClickListener {
                clickListener?.onClickNo()
                dismiss()
            }
        }
    }

    override fun onResume() {
        super.onResume()

        val width = (pixelRatio.screenShort * 0.88f).coerceAtMost(pixelRatio.toPixel(309).toFloat())
        dialog?.window?.run {
            setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
            setDimAmount(0.2f)
            setLayout(width.roundToInt(), WRAP_CONTENT)
        }
    }

    interface ClickListener {
        fun onClickYes() {}
        fun onClickNo() {}
    }

    companion object {
        fun newInstance(
            title: String? = null,
            body: String? = null,
            btnText: String? = null,
            color: Int? = null,
            showCancel: Boolean = false
        ) = CommonDialog().apply {
            arguments = bundleOf(
                "title" to title, "body" to body, "btnText" to btnText, "color" to color, "showCancel" to showCancel
            )
        }
    }
}
  • EventLiveData

one time event LiveData .

typealias SimpleEventLiveData = EventLiveData<Unit>
##  

class EventLiveData<T> : LiveData<T>() {
    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) {
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(it)
            }
        }
    }

    @MainThread
    fun emit(value: T) {
        pending.set(true)
        setValue(value)
    }
}

fun SimpleEventLiveData.emit() {
    emit(Unit)
}
  • AppEvent

SharedFlow /. LiveData Flow lifecycle Flow ViewModel .

fun SimpleSharedFlow() = MutableSharedFlow<Unit>(1, 0, DROP_OLDEST)
fun MutableSharedFlow<Unit>.emit() = tryEmit(Unit)

object AppEvent {
    val onWeathyUpdated = SimpleSharedFlow()
}

  • WeathyCardView.kt frame FrameLayout

  • fragment_home.xml MotionLayout ( )

 <androidx.constraintlayout.motion.widget.MotionLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false"
            app:layoutDescription="@xml/home_motion">

            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/guide_left"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_begin="26dp" />

            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/guide_right"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_end="26dp" />

            <ImageView
                android:id="@+id/topBlur"
                android:layout_width="match_parent"
                android:layout_height="78dp"
                android:elevation="9dp"
                android:outlineProvider="none"
                android:scaleType="fitXY"
                srcResource="@{vm.weatherSecondBackground}"
                app:layout_constraintTop_toTopOf="parent" />
   ......

  • WeathyCardView MaterialShapeDrawable ShapeAppearanceModel

  • fragment_home.xml

  • activity_developer_info.xml

Popular Sdk Projects
Popular Gradle Projects
Popular Libraries Categories
Related Searches

Get A Weekly Email With Trending Projects For These Categories
No Spam. Unsubscribe easily at any time.
Kotlin
Sdk
Gradle
Mock
Dialog
Viewmodel
Livedata
Aac