ui-extension

Maven CentralAndroid Min SDK

This is a dependency for UI (user interface) related extensions.

Configure Dependency

You can add this module to your project using the following method.

Add dependency in your project's SweetDependency configuration file.

libraries:
  com.highcapable.betterandroid:
    ui-extension:
      version: +

Configure dependency in your project build.gradle.kts.

implementation(com.highcapable.betterandroid.ui.extension)

Traditional Method

Configure dependency in your project build.gradle.kts.

implementation("com.highcapable.betterandroid:ui-extension:<version>")

Please change <version> to the version displayed at the top of this document.

Function Introduction

You can view the KDoc click hereopen in new window.

Activity Extension

When we need to start another Activity, we need to use Intent to create an Intent(this, AnotherActivity::class.java), and then call startActivity(intent) to start it.

This may not be very friendly to write, so BetterAndroid provides an extension for Activity, now you can directly use the following method to start another Activity.

The following example

// Assume this is your context.
val context: Context
// Assume AnotherActivity is your target activity.
context.startActivity<AnotherActivity>()
// You can create an intent object using the following method.
context.startActivity<AnotherActivity> {
    // Add some extra parameters here.
    putExtra("key", "value")
}
// If you need to use Intent.FLAG_ACTIVITY_NEW_TASK to start another activity,
// you can use it directly like this.
context.startActivity<AnotherActivity>(newTask = true)

If you need to start an Activity from an external app, you can use the following method.

The following example

// Assume this is your context.
val context: Context
// Assume that the app package you need to start is named com.example.app.
// Assume that the activity class you need to start is named com.example.app.MainActivity.
context.startActivity("com.example.app", "com.example.app.MainActivity")
// You can still create an intent object using the following method.
context.startActivity("com.example.app", "com.example.app.MainActivity") {
    // Add some extra parameters here.
    putExtra("key", "value")
}
// Intent.FLAG_ACTIVITY_NEW_TASK will be set by default in this method,
// this is to avoid stack overlap after starting a new app.
// If you don't need to consider this issue, you can set the newTask parameter to false.
context.startActivity("com.example.app", "com.example.app.MainActivity", newTask = false)

If you don't know the entry Activity name of the app you want to start, you can use the package name to start it directly.

This method will use getLaunchIntentForPackage internally to obtain the entry Activity.

The following example

// Assume this is your context.
val context: Context
// Assume that the app package you need to start is named com.example.app.
context.startActivity("com.example.app")

Notice

This operation requires the QUERY_ALL_PACKAGES permission or explicitly configuring a queries property list on Android 11 and later.

Please refer to Package visibility filtering on Androidopen in new window.

Tips

You can use startActivityOrElse instead of startActivity to determine whether the Activity can be started successfully.

If the startup fails, this method will not throw an exception but return false.

For the new multi-window mode in Android 7.0 and later versions, BetterAndroid provides a compatible extension for it.

For isInMultiWindowMode, you do not need to consider version compatibility issues, you only need to add a Compat at the end.

The following example

// Assume this is your activity.
val activity: Activity
// Get whether it is currently in multi-window mode.
val isInMultiWindowMode = activity.isInMultiWindowModeCompat

Fragment Extension

Fragment is an officially provided efficient fragment for Activity, but its usage is not very user-friendly.

In order to simplify Fragment related operations, BetterAndroid provides some practical extension functions for Fragment.

BetterAndroid will automatically help you introduce the androidx.fragment:fragment-ktx dependency, you can refer hereopen in new window to get started.

Notice

The commitTransaction method in 1.0.2 and previous versions has been deprecated.

In line with the principle of "not reinventing the wheel", please migrate to the commit and commitNow methods in the fragment-ktx dependency.

Get the existing FragmentManager.

BetterAndroid provides a more friendly way for FragmentActivity and Fragment to obtain the existing FragmentManager.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your Fragment.
val fragment: Fragment
// Get FragmentManager from FragmentActivity.
val fragmentManager = activity.fragmentManager()
// Get FragmentManager from Fragment.
val fragmentManager = fragment.fragmentManager()
// For Fragment, you can set the parameter parent to true to get the parent FragmentManager.
val parentFragmentManager = fragment.fragmentManager(parent = true)

Use generics to find an existing Fragment.

It can automatically help you convert the found Fragment to the current type without using the as form for coercion.

You don't need to worry about not being found or type errors at all, in which case null will be returned.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Find a fragment by ID.
val fragment = activity.fragmentManager().findFragment<YourFragment>(R.id.container)
// Find a fragment by TAG.
val fragment = activity.fragmentManager().findFragment<YourFragment>("your_fragment_tag")

Bind Fragment to FragmentActivity.

You don't need to use something like FragmentManger.beginTransaction...commit, it will be easier to complete this operation now.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your fragment.
val fragment = YourFragment()
// Bind fragment.
fragment.attachToActivity(activity)

Yes, you have completed all binding operations, by default, it will bind Fragment to the layout set by Activity through setContentView.

If you need to bind it to a custom layout, you can use the following method.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your fragment.
val fragment = YourFragment()
// Bind fragment to the layout with ID R.id.container.
fragment.attachToActivity(activity, R.id.container)

If this custom layout is not obtained by ID, but is a View object, you can use the following method.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your view.
val container: View
// Assume this is your fragment.
val fragment = YourFragment()
// Bind fragment to container. (this View must have an ID)
fragment.attachToActivity(activity, view = container)

You can also easily set the entry animation for Fragment when binding.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your fragment.
val fragment = YourFragment()
// Bind fragment and set entry animation.
fragment.attachToActivity(
    activity = activity,
    viewId = R.id.container,
    animId = R.anim.slide_in_right // Entry animation.
)

Bind Fragment to Fragment.

A Fragment can also be bound to another Fragment to form a nested relationship.

At this point you only need to replace attachToActivity with attachToFragment.

The following example

// Assume this is your parent fragment.
val parentFragment = YourParentFragment()
// Assume this is your fragment.
val fragment = YourFragment()
// Bind fragment.
fragment.attachToFragment(parentFragment)

Unbind Fragment from FragmentActivity.

You can also unbind Fragment very easily.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your fragment.
val fragment = YourFragment()
// Unbind fragment from FragmentActivity.
fragment.detachFromActivity(activity)
// If you do not fill in the parameters,
// the activity where the current fragment is located will be obtained by default.
fragment.detachFromActivity()

Unbind Fragment from Fragment.

At this point you only need to replace detachFromActivity with detachFromFragment.

The following example

// Assume this is your parent fragment.
val parentFragment = YourParentFragment()
// Assume this is your fragment.
val fragment = YourFragment()
// Unbind dragment from parent fragment.
fragment.detachFromFragment(parentFragment)
// If you do not fill in the parameters,
// the parent fragment where the current fragment is located will be obtained by default.
fragment.detachFromFragment()

In addition to bindings, you can also replace a Fragment within the same bound layout.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your fragment.
val fragment = YourFragment()
// Replace fragment into the layout with ID R.id.container.
fragment.replaceFromActivity(activity, R.id.container)

Replace Fragment in Fragment.

At this point you only need to replace replaceFromActivity with replaceFromFragment.

The following example

// Assume this is your parent fragment.
val parentFragment = YourParentFragment()
// Assume this is your fragment.
val fragment = YourFragment()
// Replace fragment into the layout with ID R.id.container.
fragment.replaceFromFragment(parentFragment, R.id.container)

Hide the current Fragment.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your parent fragment.
val parentFragment = YourParentFragment()
// Assume this is your fragment.
val fragment = YourFragment()
// Hide dragment from FragmentActivity.
fragment.hide(activity)
// Hide fragment from parent fragment.
fragment.hide(fragment = parentFragment)
// If you do not fill in the parameters,
// the parent fragment or activity where the current fragment is located will be obtained by default.
fragment.hide()

Show the current Fragment.

The following example

// Assume this is your FragmentActivity.
val activity: FragmentActivity
// Assume this is your parent fragment.
val parentFragment = YourParentFragment()
// Assume this is your fragment.
val fragment = YourFragment()
// Show fragment from FragmentActivity.
fragment.show(activity)
// Show fragment from parent fragment.
fragment.show(fragment = parentFragment)
// If you do not fill in the parameters,
// the parent fragment or activity where the current fragment is located will be obtained by default.
fragment.show()

Tips

Any binding, unbinding, replacement, showing, or hiding operation can be set up with a transition animation.

You can find the animId, enterAnimId and exitAnimId parameters in these methods, by default, no animation will be set.

Notice

Starting from version 1.0.4, BetterAndroid has removed the default transition animation and related resource files.

We believe that transition animation should be something that each developer decides for themselves, not for tool libraries.

Dimension Extension

Contents of This Section

DisplayDensityopen in new window

Friendly interface to density.

Dimension → toPxopen in new window

Dimension → toDpopen in new window

Extension for Dimension.

There are size issues everywhere in the Android UI, for example, you need to set the padding of a View to 10dp, which can be easily set in the XML layout.

But in the code you need to use TypedValue.applyDimension to convert, such code does not look very friendly.

As a result, everyone began to encapsulate a method in the form of dp2px, but this approach was still not very elegant and had problems.

BetterAndroid provides a more elegant solution for this.

Normally, you only need to pass in an existing Context or Resources to complete the conversion.

The following example

// Assume this is your context.
val context: Context
// Assume this is your resources.
val resources: Resources
// Convert 10dp to px.
valpx = 10.toPx(context)
// Convert 10dp to px.
valpx = 10.toPx(resources)
// Convert 36px to dp.
val dp = 36.toDp(context)
// Convert 36px to dp.
val dp = 36.toDp(resources)

If you are already in an environment where a Context or Resources exists, you can write it more simply as follows.

At this time, you only need to implement a DisplayDensity interface for the current instance, and you do not need to override any methods.

The following example

class YourActivity : AppCompatActivity(), DisplayDensity {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Convert 10dp to px.
        valpx = 10.dp
        // Convert 10dp to px.
        valpx = 10.toPx()
        // Convert 36px to dp.
        valdp = 36.px
        // Convert 36px to dp.
        val dp = 36.toDp()
    }
}

Notice

The DisplayDensity interface only supports being implemented into the following instances or instances inherited from them:

Context, Window, View, Resources, Fragment, Dialog

An exception will be thrown if used in an unsupported instance, if you want, you can also override the methods in the DisplayDensity interface to support it manually.

The naming method of dp, px, toPx, toDp may conflict with the naming method in Jetpack Compose, so it is not recommended to use it in such a project.

Resources Extension

Resources are a very important part of Android, they contain layouts, images, strings, etc, needed in the apps.

In order to use Resources more conveniently, BetterAndroid provides some practical extension functions for it.

Get the theme resource ID in ContextThemeWrapper.

Normally, the theme resource ID set using setTheme cannot be obtained directly. For this reason, BetterAndroid provides you with a way to obtain it through reflection.

Notice

This method may not work on all devices or be compatible with all Android versions, please test it yourself.

The following example

// Assume this is your ContextThemeWrapper.
val context: ContextThemeWrapper
// Get the set theme resource ID.
val themeResId = context.themeResId

For the attr in Resources, usually we need to create a TypedValue object, and then use Context.getTheme().resolveAttribute to fill it with the corresponding ID to obtain its own value.

For example, if we need to get the value of android.R.attr.windowBackground, we need to do this.

The following example

// Assume this is your context.
val context: Context
// Create a TypedValue object.
val typedValue = TypedValue()
// Use Context.getTheme().resolveAttribute to fill in the corresponding ID.
context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
// Get the resource ID.
val windowBackgroundId = typedValue.resourceId
// Get its own value.
val windowBackground = context.getDrawable(windowBackgroundId)

The whole process can be said to be very cumbersome, so BetterAndroid provides a simpler way for this.

Now, you just need to use the following method to get the value of itself.

The following example

// Assume this is your context.
val context: Context
// Get its own value.
val windowBackground = context.getThemeAttrsDrawable(android.R.attr.windowBackground)

If you only need to get the resource ID corresponding to attr, you can use the following method.

The following example

// Assume this is your context.
val context: Context
// Get the resource ID.
val windowBackgroundId = context.getThemeAttrsId(android.R.attr.windowBackground)

You can also only determine whether the resource ID corresponding to attr exists.

The following example

// Assume this is your context.
val context: Context
// Determine whether the resource ID corresponding to attr exists.
val hasWindowBackgroundId = context.hasThemeAttrsId(android.R.attr.windowBackground)

You can also compare two attr values to see if they are equals.

The following example

// Assume this is your context.
val context: Context
// Compare the values ​​of the two attr to see if they are equals.
val isEquals = context.isThemeAttrsIdsValueEquals(R.attr.first_attr, R.attr.second_attr)

Tips

The Context.getThemeAttrs* series of methods support common instances in Android that can be converted to actual resource objects, such as getThemeAttrsColor, getThemeAttrsInteger, getThemeAttrsString, etc.

Currently Context.getThemeAttrs* and Context.isThemeAttrsIdsValueEquals support the value contents of the following common resource IDs:

Color, ColorStateList, Drawable, Dimension, String, StringArray, IntArray, Float, Boolean

There is no way to directly obtain the Menu resource ID and parse it into a Menu object in Android.

For this reason, BetterAndroid provides a possible way to obtain it.

BetterAndroid encapsulates the method of using PopupMenu and converting the obtained content into a List<MenuItem> object through MenuInflater.

Now, you can easily use the following method to get the value of the Menu resource ID.

The following example

// Assume this is your context.
val context: Context
// Get the value of Menu resource ID.
// The obtained content is a List<MenuItem> object.
val menuItems = context.getMenuFromResource(R.menu.my_menu)

For compatibility processing of historical version systems, BetterAndroid encapsulates the methods provided by ResourcesCompat, now, you do not need to consider the issue of some methods being deprecated.

You only need to add Compat after each method to automatically make it compatible and call it like the original method, with exactly the same function.

Below is an example of compatibility handling for Drawable.

The following example

// Assume this is your context.
val context: Context
// Assume this is your resources.
val resources: Resources
// Get the drawable through context.
val drawable = context.getDrawableCompat(R.drawable.my_drawable)
// Get the drawable through resources.
val drawable = resources.getDrawableCompat(R.drawable.my_drawable, context.theme)
// You can also use generics to convert it to the corresponding type.
val drawable = context.getDrawableCompat<ColorDrawable>(R.drawable.my_background)

The following is a comparison table of the original method and the compatibility method.

Original MethodCompatibility Method
Context.getDrawableContext.getDrawableCompat
Context.getColorContext.getColorCompat
Context.getColorStateListContext.getColorStateListCompat
Context.getFloatContext.getFloatCompat
Resources.getDrawableResources.getDrawableCompat
Resources.getColorResources.getColorCompat
Resources.getColorStateListResources.getColorStateListCompat
Resources.getFloatResources.getFloatCompat
Resources.getFontContext.getFontCompat

For the property-related functions of custom View, BetterAndroid provides a TypedArray extension that can be automatically recycled.

You can directly use the obtainStyledAttributes method to create a TypedArray object without having to think about when recycle should be called.

The following example

class MyView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    init {
        obtainStyledAttributes(attrs, R.styleable.MyView) {
            val myType = it.getInteger(R.styleable.MyView_myType, 0)
        }
    }
}

Tips

BetterAndroid also provides getStringArray, getIntArray and getColorOrNull utility methods for TypedArray, which you can find them in Contents of This Section above.

Function expansion of system night mode (dark mode).

The system's night mode is determined using Configuration.UI_MODE_NIGHT_MASK, in order to make this function more understandable, you do not need to use bit operations to determine.

Because usually we don't need to care about which hosting state the current system's night mode is in, we only need to know whether the current system appearance is dark.

So BetterAndroid provides an extension in Configuration that directly uses the Boolean type for judgment.

The following example

// Assume this is your context.
val context: Context
// Determine whether the current system is in dark mode.
val isDarkMode = context.resources.configuration.isUiInNightMode

System Colors

Contents of This Section

SystemColorsopen in new window

Extension for system colors (dynamic colors).

In Android 12, the official provides us with a new feature, namely dynamic colors.

Different from the traditional wallpaper color selection, the dynamic colors in Material 3 are calculated based on the hierarchical tones of the wallpaper.

BetterAndroid encapsulates the theme colors provided by the system into SystemColors, and you can dynamically obtain the system's theme colors at the code level.

Here is an example of creating and using SystemColors.

The following example

// Assume this is your context.
val context: Context
// Create SystemColors object.
val systemColors = SystemColors.from(context)
// Get the color of android.R.color.system_accent1_100 provided by the system.
val accentColor = systemColors.systemAccentPrimary(100)
// Get the color of com.google.android.material.R.color.material_dynamic_primary50 provided by Material.
val primaryColor = systemColors.materialDynamicPrimary(50)

Not every device running Android 12 supports dynamic colors, you can use the following methods to determine.

The following example

// You can directly determine whether the current system supports dynamic colors.
val isAvailable = SystemColors.isAvailable

Retrieving colors will return the system-provided default color when not supported, but if the target device is older than Android 12, any color retrieved from SystemColors will be Color.TRANSPARENT.

The following is a complete list of the color methods supported by SystemColors and their available parameters.

Method NameAvailable Parameters
systemAccentPrimary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemAccentSecondary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemAccentTertiary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemNeutralPrimary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
systemNeutralSecondary0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000
materialDynamicPrimary0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicSecondary0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicTertiary0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicNeutral0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
materialDynamicNeutralVariant0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100

Pay Attention

You can only pass in the values among the available parameters listed above in the given method.

Other values are not supported, otherwise an exception will be thrown.

Color Extension

Color exists in the form of Integer in Android.

Although there is a class named Color to encapsulate it, many methods are added in higher versions of the system and androidx is no specific compatible implementation for it, objects passed in code context are also usually passed directly using Integer.

BetterAndroid has no reason and no need to redesign a wrapper class to manage colors, so BetterAndroid only provides relevant extensions for the Integer type.

All color objects passed in the method will be marked with the @ColorInt annotation. please also comply with the specifications provided by androidx.

Here are some relevant example uses of color extensions.

Determine how bright the color is.

This is useful when you need to decide whether to use dark text based on how bright the color is.

The following example

// Assume we have the following colors.
val color = Color.WHITE
// To determine how bright it is, you just need to use the following method.
// You will definitely get a true because this is a white color.
val isBright = color.isBrightColor

Convert color to HEX string.

The following example

// Assume we have the following colors.
val color = Color.WHITE
// To convert it to a HEX string you just need to use the following method.
// You will get a "#FFFFFFFF" with transparency.
val hexString = color.toHexColor()

Set the transparency of the current color.

The following example

// Assume we have the following colors.
val color = Color.WHITE
// You can use a floating point number from 0f-1f to set transparency.
val alphaColor = color.toAlphaColor(0.5f)
// You can also use an integer from 0-255 to set transparency.
val alphaColor = color.toAlphaColor(127)

Mix two colors.

The following example

// Assume we have the following colors.
val color1 = Color.WHITE
val color2 = Color.BLACK
// You can mix them very easily using.
val mixColor = mixColorOf(color1, color2)
// You can also set the mixing ratio, the default is 0.5f.
val mixColor = mixColorOf(color1, color2, 0.2f)

BetterAndroid also provides some extended functions for ColorStateList.

You can quickly convert existing colors to a ColorStateList with default color using the following method.

The following example

// Assume we have the following colors.
val color = Color.WHITE
// Convert it to a ColorStateList with default colors using the following method.
val colorStateList = color.toNormalColorStateList()
// You can also convert to a ColorStateList that returns null when the color is transparent.
val colorStateList = color.toNullableColorStateList()

You can also manually create a ColorStateList via AttrState.

The following example

val colorStateList = ColorStateList(
    AttrState.CHECKED to Color.WHITE,
    AttrState.PRESSED to Color.BLACK,
    AttrState.NORMAL to Color.TRANSPARENT
)

Bitmap Extension

In Android, bitmaps can be used in various places, and they are an important object used to display images.

BetterAndroid provides a series of extended functions for bitmaps, from loading to transforming, scaling, compressing and blurring them.

When loading bitmaps, you no longer need to use BitmapFactory, now there are the following methods to help you complete this operation more conveniently.

Load bitmap via a File or an InputStream.

The following example

// Assume your image is located at this path.
// Note that this path is only for demonstration, in actual situations, 
// you need to access files in the external storage.
// You need to apply for various different permissions and should use the
// Environment.getExternalStorageDirectory() method to get the path.
val imageFile = File("/storage/emulated/0/DCIM/Camera/IMG_20210901_000000.jpg")
// Create a file input stream through the file path.
val inputStream = FileInputStream("/storage/emulated/0/DCIM/Camera/IMG_20210901_000000.jpg")
// Load bitmap through file object.
val bitmap = imageFile.decodeToBitmap()
// You can configure it by passing the BitmapFactory.Options object in the method parameter.
val bitmap = imageFile.decodeToBitmap(BitmapFactory.Options().apply {
    // ...
})
// Load the bitmap through the input stream.
val bitmap = inputStream.decodeToBitmap()

Load bitmap via ByteArray.

If you have an image encrypted via Base64, you can convert it to a byte array and load the bitmap.

The following example

// Assume this is your Base64 string.
val base64String = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAA..."
// Convert Base64 string to byte array.
val byteArray = Base64.decode(base64String, Base64.DEFAULT)
// Load bitmap via byte array.
val bitmap = byteArray.decodeToBitmap()

Load bitmap via Resources.

This operation actually creates a new Bitmap object with a known Drawable resource ID.

The following example

// Assume this is your resources.
val resources: Resources
// Load bitmap via resource ID.
val bitmap = resources.createBitmap(R.drawable.my_image)

Tips

When you are not sure whether the bitmap can be loaded successfully, you can replace the loading method with decodeToBitmapOrNull and createBitmapOrNull, so that if the loading fails, it will return null instead of throwing an exception.

After a bitmap is loaded into memory or a bitmap object exists in memory, you can resave it to a file.

Kotlin's stdlib has already provided a File.writeText method for the File object, so BetterAndroid follows its example and provides a File.writeBitmap method.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Assume this is the file you want to save to.
// Note that this path is only for demonstration, in actual situations,
// you need to access files in the external storage.
// You need to apply for various different permissions and should use the
// Environment.getExternalStorageDirectory() method to get the path.
val imageFile = File("/storage/emulated/0/DCIM/Camera/IMG_20210901_000000_modified.jpg")
// Save bitmap to file.
imageFile.writeBitmap(bitmap)
// You can configure it by adjusting the format and quality parameters.
// Default is PNG format, quality is 100.
imageFile.writeBitmap(bitmap, format = Bitmap.CompressFormat.JPEG, quality = 80)

If you can actually get an OutputStream object, you can write the bitmap to the output stream using the following.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Assume this is your output stream object.
val outputStream: OutputStream
// Use the use method to automatically close the output stream.
outputStream.use {
    // Write the bitmap to the output stream.
    it.compressBitmap(bitmap)
    // You can configure it by adjusting the format and quality parameters.
    // Default is PNG format, quality is 100.
    it.compressBitmap(bitmap, format = Bitmap.CompressFormat.JPEG, quality = 80)
}

The following are extended functions for bitmap scaling, compression, and blur.

Scale bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Scale the bitmap to 100x100 size.
val zoomBitmap = bitmap.zoom(100, 100)
// Scale by multiple, default is 2 times.
val zoomBitmap = bitmap.reduce(3)

Compressed bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Compress bitmap to 100 KB size.
val compressBitmap = bitmap.compress(maxSize = 100)
// You can configure it by adjusting the format and quality parameters.
// Default is PNG format, quality is 100.
val compressBitmap = bitmap.compress(maxSize = 100, format = Bitmap.CompressFormat.JPEG, quality = 80)

Rounded bitmap.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Set the bitmap to a rounded corner with a radius of 10dp.
val roundBitmap = bitmap.round(10.toPx(context))
// You can also set the fillet radius for each corner.
val roundBitmap = bitmap.round(
    topLeft = 12.toPx(context),
    topRight = 15.toPx(context),
    bottomLeft = 10.toPx(context),
    bottomRight = 9.toPx(context)
)

To blur bitmaps, you can also use BitmapBlurFactory to accomplish this.

The following example

// Assume this is your bitmap object.
val bitmap: Bitmap
// Blur the bitmap with a blur level of 25.
val blurBitmap = bitmap.blur(25)

Notice

The blur provided here is just a general algorithm, which can quickly achieve blur effects, but may have problems with speed and performance, and cannot be used for motion blur effects.

For the bitmap blur effect in Android, you can refer to and use other possible third-party libraries.

Currently, there is no universal and complete solution, this is a historical problem in Android.

There is no reason and no need for BetterAndroid to be special, encapsulates related functions for bitmap blur.

If your app targets Android 12 and above, we recommend using the officially provided RenderEffect for blur operations, and using the RenderScript replacement renderscript-intrinsics-replacement-toolkitopen in new window (but has not been maintained for more than two years).

Drawable Extension

In some cases, we may need to use scenarios that directly manipulate Drawable objects.

BetterAndroid provides some extended functions that may be used for this purpose.

Sets padding for supported Drawable.

You can set padding on any type of Drawable using the following method, but it only works on the following types of Drawable:

RippleDrawable, LayerDrawable, ShapeDrawable, GradientDrawable

The following example

// Assume this is your drawable.
val drawable: Drawable
// Set padding for supported drawables.
drawable.setPadding(10.toPx(context))
// Or update the padding of the specified edge.
drawable.updatePadding(horizontal = 10.toPx(context))

Set padding for GradientDrawable and handle it for compatibility.

GradientDrawable that supports setting padding at the code level is only available in Android 10 and above.

You can set padding for GradientDrawable using the following compatible methods.

The following example

// Assume this is your GradientDrawable object.
val drawable: GradientDrawable
// Set padding.
drawable.setPaddingCompat(10.toPx(context), 10.toPx(context), 10.toPx(context), 10.toPx(context))
// Or use the setting method mentioned above to set the padding of all sides at once.
// This method will automatically call to setPaddingCompat for you.
drawable.setPadding(10.toPx(context))

But please note that in Android 10 and below, you need to use GradientDrawableCompat to create GradientDrawable for padding to take effect.

You can also inherit GradientDrawableCompat to implement your own GradientDrawable.

Toast Extension

Contents of This Section

Toast → toastopen in new window

Extensions for Toast.

Toast in Android has a wide range of usage scenarios, but there are various usage problems.

Earlier, ktx of androidx provided an extension method of Context.toast for Toast, but it was later removed due to notification permission issues.

Discussions about this issue can be found in Toast extensions on Contextopen in new window.

It is necessary to simplify the use of Toast in Kotlin, because sometimes the fastest way to display information is to show a Toast.

So BetterAndroid provides a toast extension method for this purpose, you can use it in the following instances or instances inherited from them:

Context, Window, Fragment, View, Dialog

You can show a Toast very simply using the following method.

The following example

// Assume this is your context.
val context: Context
// Show a toast
context.toast("Hello World!")
// You can pass in Toast.LENGTH_SHORT or Toast.LENGTH_LONG in the method parameters to set the duration.
// Default is Toast.LENGTH_SHORT.
context.toast("Hello World!", Toast.LENGTH_SHORT)

The above are all the ways to use it, BetterAndroid did not continue to customize it because the custom functions were also restricted by Android in the later period.

Please refer to Custom toasts from the background are blockedopen in new window.

Toast can only be used in the main thread, if you want to show a Toast in any thread, you only need to configure it simply like below.

The following example

// Assume this is your context.
val context: Context
// Create a new thread.
Thread {
    // Delay 1 second.
    Thread.sleep(1000)
    // Show a toast and set allowBackground to true.
    context.toast("Hello World!", allowBackground = true)
}.start() // Start it.

In this way, you can show a Toast in any thread, it should be noted that this parameter is false by default and you need to set it manually.

You should try to avoid showing Toast in non-main threads, because this may lead to some possible "black box problems" and unknown hidden dangers.

Notice

As mentioned above, in Android 13 and above, you need to define and add runtime permissions for Toast just like notifications.

Some third-party ROMs like MIUI may allow Toast to show inside the app without permission.

If there is no need for floating windows, a good suggestion is to use some other similar behavior instead of Toast, such as using the Snackbar provided by the material component.

Please refer to Notification runtime permissionopen in new window.

Window Extension

BetterAndroid provides some possible extension functions for Window.

You can modify Window.attributes in the same way as View.updateLayoutParams in androidx.

The following example

// Assume this is your window.
val window: Window
// Modify parameters in Window.attributes.
window.updateLayoutParams {
    gravity = Gravity.CENTER
    // ...
}

BetterAndroid also encapsulates the method of setting the screen brightness separately for Window.

You can modify Window.attributes.screenBrightness more conveniently using the following methods.

The following example

// Assume this is your window.
val window: Window
// Set screen brightness (0-100) for the current window.
window.updateScreenBrightness(50)
// You can also use Float type parameters to set the screen brightness.
window.updateScreenBrightness(0.5f)

If you need to restore the default screen brightness provided by the system, you can use the following method.

The following example

// Assume this is your window.
val window: Window
// Clear the set screen brightness.
window.clearScreenBrightness()

View Extension

View is an important part of the user interface, when using Kotlin, androidx provides us with extended functions for View, but it is still not perfect enough.

BetterAndroid has been improved and enriched based on the related extension functions of androidx, here are some extension functions you can use.

Get the location of View on the screen.

You can get a Point object in the following way, which contains the location of the View on the screen.

The following example

// Assume this is your view.
val view: View
// Get the location of the View on the screen.
val location = view.location
// X coordinate.
val x = location.x
// Y coordinate.
val y = location.y

Get the parent layout of the current View.

In traditional writing, we need to use View.parent to get the ViewParent object, and then use as to convert to ViewGroup to get the parent layout object.

This way of writing seems very cumbersome, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Get the parent layout of the current View.
val parent: ViewGroup = view.parent()
// Specify the type of the parent layout (if the type is known and determined).
val parent = view.parent<LinearLayout>()
// When you are not sure whether the parent layout exists, you can also use the following method.
val parent = view.parentOrNull()

Get the child layout of the current ViewGroup.

In traditional writing, we need to use ViewGroup.getChildAt to get the View object, and then use as to convert to View to get the child layout object.

This way of writing also seems very troublesome, so BetterAndroid also provides a simpler way.

The following example

// Assume this is your ViewGroup object.
val viewGroup: ViewGroup
// Get the sublayout of the current ViewGroup.
val child: View = viewGroup.child(index = 0)
// Specify the type of sublayout (if the type is known and determined).
val child = viewGroup.child<Button>(index = 0)
// Get the first sublayout of the current ViewGroup.
val firstChild: View = viewGroup.firstChild()
// Get the last sublayout of the current ViewGroup.
val lastChild: View = viewGroup.lastChild()
// When you are not sure whether the sublayout exists, you can also use the following method.
val child = viewGroup.childOrNull(index = 0)
val firstChild = viewGroup.firstChildOrNull()
val lastChild = viewGroup.lastChildOrNull()

Removes itself from the parent layout (container).

Normally, when we remove View from the parent layout, we need to use View.getParent to obtain the parent layout object, then convert it to ViewGroup, and finally call the ViewGroup.removeView method.

Now, you can more easily remove itself from the parent layout using the following method, if the parent layout does not exist or the parent layout is not a ViewGroup (which usually does not exist), this method will have no effect and no need to worry about any negative effects.

The following example

// Assume this is your view.
val view: View
// Remove itself from parent layout.
view.removeSelf()
// Use the ViewGroup.removeViewInLayout method to remove itself.
view.removeSelfInLayout()

Show or hide the input method (IME).

Normally, controlling the showing and hiding of the input method can be done automatically through the focus of the input box, but in some cases we want to be able to manually control the showing and hiding of the input method.

If the current View is in Activity, you can use the following methods to show or hide the input method.

The following example

// Assume this is your view.
val view: View
// Show input method.
view.showIme()
// Hide input method.
view.hideIme()

In Android 11 and above, the above method will use WindowInsetsController to control the showing and hiding of the input method.

When View is not in Activity or the current Android version is lower than Android 11, InputMethodManager will be used to control the showing and hiding of the input method.

Tips

If you need to use drag and drop to control the showing or hiding of input methods (window insets animation), currently BetterAndroid does not provide related extension functions.

You can refer to WindowInsetsAnimationControlleropen in new window to implement it yourself.

Notice

In Android 11 and above, it is recommended to set the android:windowSoftInputMode parameter of Activity to adjustResize to better control the showing and hiding of the input method.

If your View is not in Activity or the current Android version is lower than 11, the above solution may not be effective under certain conditions.

When Activity is first started, it is recommended to delay processing of show or hide input method events, otherwise it may be ineffective, and the interval between show and hide events should not be too short.

Simulate touch events.

The performClick method provided by View can only simulate click events, if you need to simulate touch events, you can use the View.performTouch method provided by BetterAndroid.

The parameters accepted by this method are shown in the table below.

Parameter NameParameter TypeParameter Description
downXFloatSimulates the down X coordinate of a touch event.
downYFloatSimulates the down Y coordinate of a touch event.
upXFloatSimulate the lift X coordinate of a touch event.
upYFloatSimulate the lift Y coordinate of a touch event.
durationLongThe duration of the simulated touch event. (from press to lift), in milliseconds

The following example

// Assume this is your view.
val view: View
// Simulate touch events from (15, 30) to (105, 130) for 500 milliseconds.
view.performTouch(15f, 30f, 105f, 130f, 500)

Simulate key events.

You can simulate physical keyboard or input method (IME) key events sent to the current View, for example in EditText you can simulate pressing the backspace key to delete text.

The following example

// Assume this is your view.
val view: EditText
// Simulate pressing the backspace key.
view.performKeyPressed(KeyEvent.KEYCODE_DEL)
// You can add the duration parameter at the end to set the duration (from pressing to lifting),
// the unit is milliseconds.
// Default is 150 milliseconds.
view.performKeyPressed(KeyEvent.KEYCODE_DEL, duration = 500)

Update View’s padding and margin.

androidx provides a View.updatePadding method, and BetterAndroid provides a method that can update the horizontal and vertical directions, if you only need to update the padding in these two directions, you don't need to write two repeat the value.

The following example

// Assume this is your view.
val view: View
// Update the padding in the horizontal direction.
view.updatePadding(horizontal = 10.toPx(context))
// Update the padding in the vertical direction.
view.updatePadding(vertical = 10.toPx(context))

BetterAndroid also provides a View.updateMargin method, which is used in the same way as View.updatePadding.

This method will only take effect when LayoutParams of View is MarginLayoutParams, and will have no effect in other cases.

The following example

// Assume this is your view.
val view: View
// Update the margin in the horizontal direction.
view.updateMargin(horizontal = 10.toPx(context))
// Update the margin in the vertical direction.
view.updateMargin(vertical = 10.toPx(context))
// Update left margin.
view.updateMargin(left = 10.toPx(context))

Traverse the parent layout and all child layouts.

Normally, we need to use the View.parent method to recursively traverse the parent layout and the ViewGroup.children method to recursively traverse the child layout.

BetterAndroid provides a simpler way for this, its design is inspired by the walk extension method in File provided by Kotlin.

The following example

// Assume this is your View object.
val view: View
// Assume this is your ViewGroup object.
val viewGroup: ViewGroup
// Get all parent layouts.
val parents = view.walkToRoot()
// Get all sublayouts.
val children = viewGroup.walkThroughChildren()

Gets the index of View in the parent layout.

In traditional writing, we need to use ViewGroup.indexOfChild to get the index of View in the parent layout.

This way of writing doesn't seem very friendly, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Get the index of the View in the parent layout.
// If the parent layout does not exist, -1 will be returned.
val index = view.indexOfInParent()

Sets the View's outlineProvider.

OutlineProvider of View is used to set the outer outline of View, its type is ViewOutlineProvider.

In Kotlin, we need to use view.outlineProvider = object : ViewOutlineProvider() to set it, which does not seem friendly.

So BetterAndroid provides a simpler way for this.

The following example

// Assume this is your View object.
val view: View
// Set View's outlineProvider.
view.outlineProvider { view, outline ->
    // For example, you can set the outer outline of a circle here.
    outline.setOval(0, 0, view.width, view.height)
    // Or you can set the outer outline of the rounded rectangle here.
    outline.setRoundRect(0, 0, view.width, view.height, 10f.toPx(context))
}
// Remember to set clipToOutline to true after setting outlineProvider.
view.clipToOutline = true

Manually create a LayoutParams object.

LayoutParams is the layout parameter of View, and its type depends on the parent layout of View, for example, the LayoutParams of LinearLayout is LinearLayout.LayoutParams.

Sometimes, you may need to manually create this object and set it into View, in this case, BetterAndroid provides the ViewLayoutParams method, now, you can save yourself the step of creating an object using the super long ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).

The following example

// Assume this is your view.
val view: View
// Create a LinearLayout.LayoutParams object, by default, width and height are both WRAP_CONTENT.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>()
// Set both width and height to MATCH_PARENT.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>(marchParent = true)
// Just set width to MATCH_PARENT.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>(widthMatchParent = true)
// Manually set width and height.
val layoutParams = ViewLayoutParams<LinearLayout.LayoutParams>(50.toPx(context), 30.toPx(context))
// Set to view.
view.layoutParams = layoutParams

LayoutInflater Extension

Normally we can use getLayoutInflater or LayoutInflater.from(context) in Activity to create a LayoutInflater object, and then use the inflate method to inflate the layout.

BetterAndroid simplifies this step for you, now, you can use layoutInflater in Context to get the LayoutInflater object, and then use the inflate method to load the layout.

The following example

// Assume this is your ViewGroup.
val view: ViewGroup
// Assume this is your context.
val context: Context
// You can inflate the layout in ViewGroup using the following method.
val myView = context.layoutInflater.inflate(R.layout.my_layout, root = view)
// You can set the attachToRoot parameter to decide whether to add it to the ViewGroup at the same time.
val myView = context.layoutInflater.inflate(R.layout.my_layout, root = view, attachToRoot = true)

TextView Extension

TextView is one of the most commonly used components in Android, BetterAndroid provides TextView with some extended functions that are more convenient to use in Kotlin.

Determine whether there is an ellipsis in TextView.

The following example

// Assume this is your TextView.
val textView: TextView
// Determine whether there is an ellipsis in TextView.
val isEllipsize = textView.isEllipsize

Get and set the underline effect of TextView.

The following example

// Assume this is your TextView.
val textView: TextView
// Get whether the TextView has an underline effect.
val isUnderline = textView.isUnderline
// Set the underline effect of TextView.
textView.isUnderline = true

Get and set the strike through effect of TextView.

The following example

// Assume this is your TextView.
val textView: TextView
// Get whether the TextView has a strike through effect.
val isStrikeThrough = textView.isStrikeThrough
// Set the strikethrough effect of TextView.
textView.isStrikeThrough = true

Notice

When setting the underline or strikethrough effect, you may need to manually enable anti-aliasing for TextView using paint.isAntiAlias = true to achieve better results, this is a historical issue with drawing performance in Android.

Get and set the text color of TextView.

Although you can use the TextView.setTextColor method to set the text color, it is not well recognized as the Getter and Setter methods in Kotlin because the corresponding TextView.getTextColors is a ColorStateList object.

So BetterAndroid has added an extension for this function, now, you can use the following methods to get and set the text color of TextView.

// Assume this is your TextView.
val textView: TextView
// Get the text color of TextView.
val textColor = textView.textColor
// Set the text color of TextView.
textView.textColor = Color.RED

Update the text of EditText.

EditText inherits from TextView, directly using setText(...) to update the text will cause the cursor position to still be at the first position.

BetterAndroid provides a more convenient way for this, it will automatically set setSelection for you according to the length of the text to keep the cursor position at the end of the text.

The following example

// Assume this is your EditText.
val editText: EditText
// Update the text of EditText.
editText.updateText("Hello World!")

Clear the text of TextView.

Using setText("") or text = "" does not seem very friendly, so BetterAndroid provides a simpler way for this.

The following example

// Assume this is your TextView.
val textView: TextView
// Clear the text of TextView.
textView.clear()

Update TextView's Typeface.

Typeface is a font in Android, sometimes we only need to care about the thickness and italics of the font without setting a specific font.

BetterAndroid provides a simpler way for this, now, you can update the Typeface of TextView using the following method.

The following example

// Assume this is your TextView.
val textView: TextView
// Only set the TextView's font to bold.
textView.updateTypeface(Typeface.BOLD)

Updated CompoundDrawables of TextView.

CompoundDrawables are compound drawings in TextView that contain Drawable objects.

Using the traditional setCompoundDrawables method requires passing in four parameters and filling in null for unnecessary parameters, which seems very troublesome.

BetterAndroid provides a simpler way for this, now, you can directly update the CompoundDrawables of TextView using the following method.

The following example

// Assume this is your TextView.
val textView: TextView
// Update CompoundDrawables of TextView.
// We only need to fill in the parameters in the required direction,
// and the parameters in other directions will be automatically reset to the original CompoundDrawables.
textView.updateCompoundDrawables(
    left = R.drawable.ic_left,
    top = R.drawable.ic_top,
    right = R.drawable.ic_right,
    bottom = R.drawable.ic_bottom
)
// Use setCompoundDrawablesWithIntrinsicBounds to update CompoundDrawables.
textView.updateCompoundDrawablesWithIntrinsicBounds(
    left = R.drawable.ic_left,
    top = R.drawable.ic_top,
    right = R.drawable.ic_right,
    bottom = R.drawable.ic_bottom
)

Set the input limits (digits) of the TextView.

This attribute can only be conveniently set in XML using digits, but if you need to modify it dynamically, you will need the TextView.setKeyListener method.

For this purpose, BetterAndroid provides a setDigits method for TextView, you can use the following method to set the input limit of TextView.

Since the input function is completed by EditText, and EditText inherits from TextView, this method is generally only used in EditText.

The following example

// Assume this is your EditText.
val editText: EditText
// Set the input box to only input numbers.
editText.setDigits("0123456789")
// Set the input box to only input lowercase letters.
editText.setDigits("abcdefghijklmnopqrstuvwxyz")
// The second parameter is inputType, you can limit the display behavior of the input box here.
// For example, only numbers can be entered and displayed as passwords.
editText.setDigits("0123456789", InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD)
// The third parameter is locale,
// where you can set the country area of the input box. (only available in Android 8 and above)
// For example, set to mainland China.
editText.setDigits("0123456789", locale = Locale.CHINA)

ViewBinding Extension

ViewBinding is a new feature in Android, which can help us operate components in the layout more conveniently.

But obviously the official method of using it is not open, you can get the interface ViewBinding of the generated class, and there is no implementation of inflate and other methods, you can only use a method like ActivityMainBinding.inflate(layoutInflater) to inflate the layout.

So BetterAndroid performed reflection processing on it to obtain the inflate method and extract the object type through generics.

These designs are partly inspired by the ViewBindingKTXopen in new window project, many thanks to the author of this project.

Now you can create a ViewBindingBuilder using the following and pass it to wherever needed for manipulation.

The following example

// Create a ViewBindingBuilder.
val builder = ViewBinding<ActivityMainBinding>()
// Inflate the layout when needed.
val binding = builder.inflate(layoutInflater)
// Assume this is your View.
val view: View
// You can also bind to an existing View.
val binding = builder.bind(view)

In this way we have implemented the passing of ViewBinding, and you can use it anywhere.

You can also use delegation to use ViewBinding in Activity and other places.

The following example

class YourActvity : Activity() {

    val binding: ActivityMainBinding by viewBinding()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    }
}

If you need to encapsulate ViewBinding into the parent class and bind it to the subclass using generics, you can use the following method.

First we need to create a parent class.

The following example

open class YourBaseActvity<VB : ViewBinding> : Activity() {

    lateinit var binding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       // Inflate the view binding.
       binding = ViewBindingBuilder.fromGeneric<VB>(this).inflate(layoutInflater)
       setContentView(binding.root)
    }
}

Then inherit this parent class as a global object into the subclass.

The following example

class YourActivity : YourBaseActivity<ActivityMainBinding>() {

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       binding.mainText.text = "Hello World!"
    }
}

Tips

You can also refer to ui-component → Activity to directly use the encapsulated AppBindingActivity or refer to ui-component → Fragment directly uses the encapsulated AppBindingFragment.

Pay Attention

If your application was compiled with obfuscation enabled, you need to refer to R8 & Proguard Obfuscate to correctly configure the obfuscation rules.