I want to observe onFling Function to detect Velocityx and Velocity Means Swiping force/Velocity. In android we attach this to the android view.But donot find a way how to call this in Jetpack compose or alternative function for this in jetpack compose...?
Please take the Animation-in-Compose codelab to understand better, but for now, here's how you can achieve something similar
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
// This `Animatable` stores the horizontal offset for the element.
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// Wait for a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
// Interrupt any ongoing animation.
offsetX.stop()
// Prepare for drag events and record velocity of a fling.
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Record the position after offset
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
// Overwrite the `Animatable` value while the element is dragged.
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
change.consumePositionChange()
}
}
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Calculate where the element eventually settles after the fling animation.
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
// The animation should end as soon as it reaches these bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back to the default position.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
}
}
}
// Apply the horizontal offset to the element.
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
I would not recommend trying to comprehend this code just from here, you should probably refer to the documentation at hand, while taking the codelab as well. This implements a swipe-to-dismiss functionality on a simple list item in a compose-sample app, the code to which you will find linked to in the start of the codelab. Here are all the sample apps published for compose, for your reference.
I am currently able to update the status bar text color from light to dark using the following inside my base activity:
private fun toggleStatusBarTextColor(light: Boolean) {
// clear any existing flags
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
if(light) {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
}
}
systemUiVisibility is now showing deprecated on API 30, and although the deprecated methods will still function for the time being, I would prefer to replace them with the newer way to accomplish this. I have read that we should now use the WindowInsetsController functions, but it is not clear to be how to accomplish this from the docs. Can someone point me in the right direction?
For API 30 you can use WindowInsetsController.setSystemBarsAppearance (int appearance, int mask):
To make status bar light:
window.insetsController?.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
)
To clear the flag:
window.insetsController?.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
)
Note that getInsetsController is nullable hence the ? check.
Alternatively (and for lower APIs) you can use WindowInsetControllerCompat:
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
windowInsetController?.isAppearanceLightStatusBars = true // or false
Note: if clearing flag doesn't work check value of window.decorView.windowSystemUiVisibility - if it contains View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR that means your view hierarchy contains a View with this flag which is propagated and affects systemUiVisibility calculation.
I, as others, couldn't get the new API that #Pawel recommended to work on all Android OS versions. I unfortunately found I had to use both the older API as well as the new to get it working on Android 11 & below:
fun setStatusBarLightText(window: Window, isLight: Boolean) {
setStatusBarLightTextOldApi(window, isLight)
setStatusBarLightTextNewApi(window, isLight)
}
private fun setStatusBarLightTextOldApi(window: Window, isLight: Boolean) {
val decorView = window.decorView
decorView.systemUiVisibility =
if (isLight) {
decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
private fun setStatusBarLightTextNewApi(window: Window, isLightText: Boolean) {
ViewCompat.getWindowInsetsController(window.decorView)?.apply {
// Light text == dark status bar
isAppearanceLightStatusBars = !isLightText
}
}
I Have the below layout.
<CoordinatorLayout>
<AppBarLayout>
<CollapsingToolbarLayout>
<Toolbar>
</CollapsingToolbarLayout>
<View1>
<View2>
</AppBarLayout>
<RecyclerView>(appbar_scrolling_view_behavior)
On one use case, I use an empty state viewholder to show that there are empty items from api.Also I have to hardcode the height of the recyclerview relative to the screenheight. As a result, on scrolling of the recyclerview fully up there is some space left below the empty state of the recyclerview.I do not want this space.Can anyone please help me?
recyclerview.scrollToPosition(0)
val params = appbar.layoutParams as CoordinatorLayout.LayoutParams
val behavior = params.behavior as AppBarLayout.Behavior?
if (behavior != null) {
val valueAnimator: ValueAnimator = ValueAnimator.ofInt()
valueAnimator.interpolator = DecelerateInterpolator()
valueAnimator.addUpdateListener { animation ->
behavior.topAndBottomOffset = (animation.animatedValue as Int)
appbar.requestLayout()
}
valueAnimator.setIntValues(
0, -appbar.totalScrollRange + view2.measuredHeight
)
valueAnimator.duration = 50
valueAnimator.start()
}
Found this approach to work after trying other approaches.
I'm currently trying to make a universal method for an animation overlay. Works this way: pass activity as a parameter -> method attaches required views onto Activity's rootview, invalidates it and starts in this case rotate animation. However the animation's pivot doesn't apply to view correctly, everything works except that, so it just rotates from View's X and Y, which is incorrect.
The method:
fun createLoading(activity: Activity, activityWidth: Int): Animation {
val loadingAnimation = AnimationUtils.loadAnimation(activity, R.anim.animation_loading_rotate)
val parent = activity.window.decorView.findViewById<ViewGroup>(android.R.id.content)
val loadingBg = ImageView(activity)
loadingBg.tag = "customLoadingBg"
loadingBg.layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT)
loadingBg.visibility = View.VISIBLE
loadingBg.setImageResource(R.drawable.darken_background)
val loadingImage = ImageView(activity)
loadingImage.tag = "customLoadingImage"
loadingImage.setImageResource(R.drawable.icon_web)
loadingImage.visibility = View.VISIBLE
loadingImage.layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT) //TODO pivot doesn't work. Again...
loadingImage.layoutParams.width = (activityWidth * 0.1).toInt()
loadingImage.layoutParams.height = (activityWidth * 0.1).toInt()
loadingImage.pivotX = (activityWidth * 0.05).toFloat()
loadingImage.pivotY = (activityWidth * 0.05).toFloat()
loadingImage.x = ((activityWidth / 2 - (activityWidth * 0.1 / 2).toInt()).toFloat())
loadingImage.y = 0f
parent.addView(loadingBg)
parent.addView(loadingImage)
loadingAnimation!!.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
parent.removeView(parent.findViewWithTag<ImageView>("customLoadingImage"))
parent.removeView(parent.findViewWithTag<FrameLayout>("customLoadingBg"))
}
override fun onAnimationStart(animation: Animation?) {
loadingBg.alpha = 0.5f
loadingImage.pivotX = (activityWidth * 0.05).toFloat()
loadingImage.pivotY = (activityWidth * 0.05).toFloat()
Log.d("loadingimg_pivotX", loadingImage.pivotX.toString())
loadingBg.bringToFront()
loadingImage.bringToFront()
}
})
loadingImage.post {
activity.runOnUiThread {
parent.invalidate()
parent.getChildAt(parent.childCount - 1).startAnimation(loadingAnimation)
}
}
return loadingAnimation
}
As you can see I even tried to put it into the animation's onAnimationStart method, but doesn't work.
I even tried using a predefined XML animation and applying to the view, but neither of those worked out for me.
Appreciate any ideas.
EDIT:
Declaration of the R.anim.animation_loading_rotate
android:duration="750"
android:fromDegrees="0"
android:interpolator="#android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:toDegrees="360"
Would be nice to have sample, to check it locally. I'm not sure, about which pivot you are talking about in this sample. Pivot in our case, should be relative to the View under animation, not parent. So in our case, if it's rotation, the right changes would be next.
loadingImage.pivotX = (loadingImage.getWidth() * 0.5).toFloat()
loadingImage.pivotY = (loadingImage.getHeight() * 0.5).toFloat()
Last note, I strongly recommend to stick with Android Property Animation not View Animation. You can simply replace your sample with ObjectAnimator, and probably you issue could be resolved. Because ObjectAnimator not just animate but navigates View changes, where simple View animation just redrawing object. Check more detail from the documentation.
I am trying to check to see whether the android navigation bar is present on load so that I can adjust a layout accordingly, does anyone have any suggestions?
This is the navigation bar I am trying to detect:
P.S. All I have found so far are 'bad' ways to try and remove the bar, which I dont want to do.
Took me some time but I've found a more reliable way than relying on hasPermanentMenuKey() which doesn't work for newer phones like the HTC One which have no menu key but do have home & back keys so don't need (or show) the soft navigation bar. To get around this try the following code which checks for a back button too:
boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey();
boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
if(!hasMenuKey && !hasBackKey) {
// Do whatever you need to do, this device has a navigation bar
}
There's no reliable way to check for a navigation bar. Using KeyCharacterMap.deviceHasKey you can check if certain physical keys are present on the device, but this information is not very useful since devices with physical keys can still have a navigation bar. Devices like the OnePlus One, or any device running a custom rom, have an option in the settings that disables the physical keys, and adds a navigation bar. There's no way to check if this option is enabled, and deviceHasKey still returns true for the keys that are disabled by this option.
This is the closest you can get:
boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
boolean hasHomeKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME);
if (hasBackKey && hasHomeKey) {
// no navigation bar, unless it is enabled in the settings
} else {
// 99% sure there's a navigation bar
}
If the back and home button are not both physically present on the device, it must have a navigation bar, because the user otherwise wouldn't be able to navigate at all. However, you can never be 100% sure about this, since manufacturers can implement deviceHasKey wrong.
Another solution
(a part of my class UtilsUISystem )
public static boolean hasNavBar (Resources resources)
{
//Emulator
if (Build.FINGERPRINT.startsWith("generic"))
return true;
int id = resources.getIdentifier("config_showNavigationBar", "bool", "android");
return id > 0 && resources.getBoolean(id);
}
Here is a quick answer that combines Pauland's and Philask's solutions. I'm afraid I don't have enough devices available to test if it works everywhere, though. I'd be interested to hear others' results.
boolean hasNavBar(Context context) {
Resources resources = context.getResources();
int id = resources.getIdentifier("config_showNavigationBar", "bool", "android");
if (id > 0) {
return resources.getBoolean(id);
} else { // Check for keys
boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey();
boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
return !hasMenuKey && !hasBackKey;
}
}
I've done like this, it works on every device I tested, and even on emulators:
public static boolean hasNavigationBar(Activity activity) {
Rect rectangle = new Rect();
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rectangle);
activity.getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);
return displayMetrics.heightPixels != (rectangle.top + rectangle.height());
}
you could add this code to your activity's onCreate() method:
View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener
(new View.OnSystemUiVisibilityChangeListener() {
#Override
public void onSystemUiVisibilityChange(int visibility) {
if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
// TODO: The navigation bar is visible. Make any desired
// adjustments to your UI, such as showing the action bar or
// other navigational controls.
} else {
// TODO: The navigation bar is NOT visible. Make any desired
// adjustments to your UI, such as hiding the action bar or
// other navigational controls.
}
}
});
This method worked for me
int id = getResources().getIdentifier("config_showNavigationBar","bool","android");
boolean result = id > 0 && getResources().getBoolean(id);
//
if(result) {
// Do whatever you need to do, this device has a soft Navigation Bar
}
It worked for me and tested in many devices.
Something that should probably work better is to measure the screen.
Starting with API 17 there's getWindowManager().getDefaultDisplay().getRealSize(), which can be compared to size returned by getWindowManager().getDefaultDisplay().getSize().
If you get different results I think it's safe to say that there is a nav bar and if you get the same results there isn't one. One thing to pay attention to is your target SDK and supported screens, which might cause the result of getSize() to be scaled if Android thinks your app wouldn't work well on the current device without scaling.
Below API 17 you can measure the screen via getWindowManager().getDefaultDisplay().getMetrics() in both landscape and portrait mode, and again, different results probably mean there's a nav bar.
However, if you get the same results, you don't actually know, as phones can keep the nav bar on the shorter edge even when in landscape. An educated guess would be that if either the width or the height is 4% to 8% smaller than standard sizes like 1280x800, 1280x720, 1024x600, while the other dimension is equal, then again there probably is a nav bar. Don't bet on it, though. There are too many resolutions, which differ too little from one another for this to work well.
boolean hasNavBar(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// navigation bar was introduced in Android 4.0 (API level 14)
Resources resources = context.getResources();
int id = resources.getIdentifier("config_showNavigationBar", "bool", "android");
if (id > 0) {
return resources.getBoolean(id);
} else { // Check for keys
boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey();
boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
return !hasMenuKey && !hasBackKey;
}
} else {
return false;
}
}
On Android 10 (API level 29), you can also check for the bottom window inset:
#RequiresApi(api = Build.VERSION_CODES.Q)
private boolean hasNavigationBar() {
final WindowInsets windowInsets = getWindow().getDecorView().getRootWindowInsets();
if (windowInsets == null) {
throw new RuntimeException("Window is not attached");
}
return windowInsets.getTappableElementInsets().bottom > 0;
}
Note that the window has to be attached for getRootWindowInsets() to return a non-null value, so you likely want to call this in onAttachedToWindow.
This solution is also used by LineageOS's launcher app Trebuchet (source), which is how I learned of it.
I see the answers above, I want to indicate
that the "not exist" can be regard as the height of 0;
so it can be like this:
public static int getScreenH(Context context) {
DisplayMetrics dm = new DisplayMetrics();
dm = context.getResources().getDisplayMetrics();
int h = dm.heightPixels;
return h;
}
public static int getDpi(Context context) {
DisplayMetrics displayMetrics1 = context.getResources().getDisplayMetrics();
int height1 = displayMetrics1.heightPixels;
int dpi = 0;
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics displayMetrics = new DisplayMetrics();
#SuppressWarnings("rawtypes")
Class c;
try {
c = Class.forName("android.view.Display");
#SuppressWarnings("unchecked")
Method method = c.getMethod("getRealMetrics", DisplayMetrics.class);
method.invoke(display, displayMetrics);
dpi = displayMetrics.heightPixels;
} catch (Exception e) {
e.printStackTrace();
}
return dpi;
}
public static int getBottomStatusHeight(Context context) {
int totalHeight = getDpi(context);
int contentHeight = getScreenH(context);
return totalHeight - contentHeight;
}
```
Solution: Only devices without permanent hardware keys have the navigation bar hence you can check for the API version and use hasPermanentMenuKey() to find hardware keys
boolean hasMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey();