Make fragment clickable when navigation drawer is opened - android

My problem is as follows: I lock the navigation drawer menu setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN) in the landscape mode of the tablet, but I need the fragment from the right to be active, so I can click it with navigation always opened. But I dont know how to do it. Please help.

There are a few things you need to do:
Disable the layout fading by setting a transparent color:
drawer.setScrimColor(Color.TRANSPARENT);
Lock the drawer
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
Create a custom drawer class which allows clicking through when in locked mode:
public class CustomDrawer extends DrawerLayout {
public CustomDrawer(Context context) {
super(context);
}
public CustomDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
View drawer = getChildAt(1);
if (getDrawerLockMode(drawer) == LOCK_MODE_LOCKED_OPEN && ev.getRawX() > drawer.getWidth()) {
return false;
} else {
return super.onInterceptTouchEvent(ev);
}
}
}
Use this class in xml:
<com.example.myapplication.CustomDrawer
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
</FrameLayout>
<ListView android:layout_width="100dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#111"/>
</com.example.myapplication.CustomDrawer>

This is a tricky one. When the drawer is open, it intercepts your touch events which trigger the close of the drawer. In order to prevent that, you need to subclass your DrawerLayout and override the onInterceptTouchEvent method:
public class CustomDrawerLayout extends DrawerLayout
{
private View rightView;
private int mTouchSlop;
public CustomDrawerLayout (Context context)
{
this(context, null);
}
public CustomDrawerLayout (Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public CustomDrawerLayout (Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(ViewConfiguration.get(context));
}
public void setRightView (View v)
{
this.rightView = v;
}
#Override
public boolean onInterceptTouchEvent (MotionEvent ev)
{
boolean result = super.onInterceptTouchEvent(ev);
if (rightView != null && isDrawerOpen(rightView))
{
DrawerLayout.LayoutParams layoutParams = (DrawerLayout.LayoutParams) rightView.getLayoutParams();
if (layoutParams.gravity == Gravity.END)
{
// This is true when the position.x of the event is happening on the left of the drawer (with gravity END)
if (ev.getX() < rightView.getX() && ev.getX() > mTouchSlop)
{
result = false;
}
}
}
return result;
}
}
This is my code working with a right drawer. I'm sure you can adapt this for your left drawer. You might also want to disable the shadow:
mDrawerLayout.setScrimColor(Color.TRANSPARENT);

Thanks for #Simas's solution!
I found if an item which user click is quite near drawerView, use ev.rawX is not appropriate. Furthermore, I add other gravity check to determinate interception.
class ContentTouchableDrawer #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : DrawerLayout(context, attrs) {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val drawer: View = getChildAt(1)
logt("drawer : $drawer")
logt("drawer : width = ${drawer.width}")
logt("drawer : x = ${drawer.x}")
logt("drawer : eventRawX = ${ev.rawX}")
logt("drawer : eventX = ${ev.x}")
val drawerGravity = (drawer.layoutParams as LayoutParams).gravity
val result = when(drawerGravity){
Gravity.RIGHT, GravityCompat.END -> ev.x < drawer.x
Gravity.LEFT, GravityCompat.START -> ev.x > drawer.width
//Gravity.NO_GRAVITY
else -> false
}
return if (getDrawerLockMode(drawer) == LOCK_MODE_LOCKED_OPEN && result) {
false
} else {
super.onInterceptTouchEvent(ev)
}
}
}

Related

How to Disable Sound Effects in Android TV app without Muting all System Sounds

I would like to disable sound effects when browsing over RecycleView items and also clicking sounds in an Android TV app. But, I do not want to disable all other sounds (e.g., There is Exoplayer in the app that its output sounds should not be muted).
I noticed there are some other questions similar to this on Stackoverflow and the suggested solutions are:
Disable Sound effect in the Layout Files by setting android:soundEffectsEnabled="false" (I put this in every Layout). However, this does not have any effect and there is still clicking and item browsing sound effects.
Disable sound effects using AudioManager. I tried the following:
audioManager.adjustStreamVolume(AudioManager.STREAM_NOTIFICATION, AudioManager.ADJUST_MUTE, 0); and audioManager.adjustStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_MUTE, 0); These mute all app sounds including Media sounds.
I would be grateful if someone can help with this issue. Thanks
Finally I found a solution for this problem.
Issue 1: Disabling sound effect on pressing DPAD_CENTER key. I could resolve this issue by programmatically disabling sound effect in CardPresenter (for Leanback ListRowPresenter) and CardAdapter (for RecyclerView).
Issue 2: Disabling sound effect on pressing DPAD navigation keys (DPAD_RIGHT, DPAD_LEFT, ...). Digging into the ViewRootImpl.java class, it turns out that navigation sound is always played without checking the soundEffect flag. Here is parts of the code in ViewRootImpl.java
if (v.requestFocus(direction, mTempRect)) {
boolean isFastScrolling = event.getRepeatCount() > 0;
playSoundEffect(
SoundEffectConstants.getConstantForFocusDirection(direction,
isFastScrolling));
return true;
So a workaround that I came up with is to override the requestFocus method in my views and always return false to prevent playing sound effect.
Code for Leanback ListRowPresenter:
CardPresenter.java
public class CardPresenter extends Presenter {
....
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
....
Context mContext = parent.getContext();
CustomImageCardView mCardView = new CustomImageCardView(mContext);
mCardView.setSoundEffectsEnabled(false);
return new ViewHolder(mCardView);
}
CustomImageCardView.java
public class CustomImageCardView extends ImageCardView {
public CustomImageCardView(Context context, int themeResId) {
super(context, themeResId);
}
public CustomImageCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomImageCardView(Context context) {
super(context);
}
public CustomImageCardView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
super.requestFocus(direction, previouslyFocusedRect);
return false;
}
}
Code for RecyclerView:
CardAdapter.java
public class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> {
...
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
View view = mLayoutInflater.inflate(R.layout.recycler_view, viewGroup, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
view.setFocusable(true);
view.setSoundEffectsEnabled(false);
}
mViewHolder = new ViewHolder(view);
return mViewHolder;
}
CustomLinearLayout.java (Root View for Recycler View)
public class CustomLinearLayout extends LinearLayout {
public CustomLinearLayout(Context context) {
super(context);
}
public CustomLinearLayout(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomLinearLayout(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void playSoundEffect(int soundConstant) {
super.playSoundEffect(soundConstant);
}
#Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
super.requestFocus(direction, previouslyFocusedRect);
return false;
}
}
If you just want to mute android tv navigation system sound effect and there is no custom navigation behavior, I found a way by overriding onKeyDown.
First, I added a GlobalFocusChangeListener at Activity and Dialog to listen and keep the reference of focused view.
window.decorView.viewTreeObserver.addOnGlobalFocusChangeListener { oldFocus, newFocus ->
focusView = newFocus
}
Second, I overrided onKeyDown method at Activity and Dialog and implement like this.
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
focusView?.let {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> View.FOCUS_UP
KeyEvent.KEYCODE_DPAD_DOWN -> View.FOCUS_DOWN
KeyEvent.KEYCODE_DPAD_LEFT -> View.FOCUS_LEFT
KeyEvent.KEYCODE_DPAD_RIGHT -> View.FOCUS_RIGHT
else -> null
}?.let { direction ->
val nextFocusView = it.focusSearch(direction)
if (nextFocusView != null) {
nextFocusView.requestFocus()
return true
}
}
}
return super.onKeyDown(keyCode, event)
}
This is work on android tv emulator and my Xiaomi TV. I think this change will not effect any touch behavior on phone or tablet.

Restrict user scrolling in RecyclerView

In my project I use a RecyclerView that I only want to scroll by calling the startSmoothScroll() method of the LayoutManager:
private fun next(){
val layoutManager = pager.layoutManager as BattlePageLayoutManager
layoutManager.startSmoothScroll(smoothScroller(layoutManager.findFirstVisibleItemPosition() + 1))
layoutManager.finishScroll()
}
I do not want the user to be able to scroll manually, e. g. by swiping.
I already tried to achieve this through overriding the method onInterceptTouchEvent() of the parent FrameLayout.
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN){
startClickTime = System.currentTimeMillis()
startX = ev.x
startY = ev.y
}
val allowEvent = (System.currentTimeMillis() - startClickTime) < 1000 && (startX-ev.x).absoluteValue < 15 && (startY-ev.y).absoluteValue < 15
return !allowEvent
}
That worked basically, but it occured that after double-tapping the View users are able to scroll by themselves.
Do you have any other ideas to approach this?
Did you try overriding canScrollVertically() method in the LayoutManager?
mLayoutManager = new LinearLayoutManager(getActivity()) {
#Override
public boolean canScrollVertically() {
return false;
}
};
Edit:
Create your own implementation of RecyclerView which it disables the touch event while scrolling is performing. Then you have to change the RecyclerView class in the xml file and Fragment/Activity with it.
Find here an example in Kotlin
class MyRecyclerView : RecyclerView {
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
return if (scrollState != RecyclerView.SCROLL_STATE_IDLE) false else super.onInterceptTouchEvent(e)
}
}
And in Java
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if(getScrollState() != SCROLL_STATE_IDLE)
return false;
return super.onInterceptTouchEvent(e);
}
}
You might want to block the user interaction with RecyclerView, not with FrameLayout itself.
Check RecyclerView.OnItemTouchListener.
In your RecyclerView, you can implement OnItemTouchListener and override every method to do nothing.
That will block the user interaction with RecyclerView, making scroll not happen.

Android fitsSystemWindows not working when replacing fragments

I have SingleFramgnetActivity whose purpose is only to hold and replace fragments inside it.
layout looks like this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".SingleFragmentActivity"
>
<include layout="#layout/toolbar"/>
<FrameLayout
android:id="#+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
I'm replacing the Fragments inside the FrameLayout. When I set the fitsSystemWindows to true on the Fragment layout, it is not responding. Actually it is working only when Activity is created, but once I replace the Fragment inside the FrameLayout, the fitsSystemWindows parameter is ignored and the layout is below the status bar and navigation bar.
I found some solution with custom FrameLayout which is using deprecated methods, but for some reason it is not working for me (same result as with normal FrameLayout) and I also do not like the idea to use deprecated methods.
Your FrameLayout is not aware of window inset sizes, because it's parent - LinearLayout hasn't dispatched it any. As a workaround, you can subclass LinearLayout and pass insets to children on your own:
#TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
#Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
int childCount = getChildCount();
for (int index = 0; index < childCount; index++)
getChildAt(index).dispatchApplyWindowInsets(insets); // let children know about WindowInsets
return insets;
}
You can have a look to my this answer, which will explain detailed how this works, and also how to use ViewCompat.setOnApplyWindowInsetsListener API.
You could also build a custom WindowInsetsFrameLayout and use a OnHierarchyChangedListener to request applying the insets again:
public WindowInsetsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// Look for replaced fragments and apply the insets again.
setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
#Override
public void onChildViewAdded(View parent, View child) {
requestApplyInsets();
}
#Override
public void onChildViewRemoved(View parent, View child) {
}
});
}
Check out this detailed answer: https://stackoverflow.com/a/47349880/3979479
I think the problem revolves around onApplyWindowInsets getting called before the fragment view hierarchy gets attached. An effective solution is to get the following override on a view somewhere in the view hierarchy of the fragment.
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// force window insets to get re-applied if we're being attached by a fragment.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
requestApplyInsets();
} else {
//noinspection deprecation
requestFitSystemWindows();
}
}
A complete solution (if you don't have to use CoordinatorLayout) follows. Make sure fitSystemWindows=true does not appear ANYWHERE in views higher in the heirarchy. Maybe not anywhere else. I suspect (but am not sure) that consumeSystemWindowInsets eats the insets for views that are further on in the layout order of the view tree.
package com.twoplay.xcontrols;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowInsets;
import android.widget.FrameLayout;
public class FitSystemWindowsLayout extends FrameLayout {
private boolean mFit = true;
public FitSystemWindowsLayout(final Context context) {
super(context);
init();
}
public FitSystemWindowsLayout(final Context context, final AttributeSet attrs) {
super(context, attrs);
init();
}
public FitSystemWindowsLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setFitsSystemWindows(true);
}
public boolean isFit() {
return mFit;
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
requestApplyInsets();
} else {
//noinspection deprecation
requestFitSystemWindows();
}
}
public void setFit(final boolean fit) {
if (mFit == fit) {
return;
}
mFit = fit;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
requestApplyInsets();
} else {
//noinspection deprecation
requestFitSystemWindows();
}
}
#SuppressWarnings("deprecation")
#Override
protected boolean fitSystemWindows(final Rect insets) {
if (mFit) {
setPadding(
insets.left,
insets.top,
insets.right,
insets.bottom
);
return true;
} else {
setPadding(0, 0, 0, 0);
return false;
}
}
#TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
#Override
public WindowInsets onApplyWindowInsets(final WindowInsets insets) {
if (mFit) {
setPadding(
insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom()
);
return insets.consumeSystemWindowInsets();
} else {
setPadding(0, 0, 0, 0);
return insets;
}
}
}
Suspicion, not fact: that only one view in the entire hierarchy gets a chance to eat the window insets, UNLESS you have CoordinatorLayout in the hierarchy, which allows more than one direct child to have FitSystemWindow=true. If you do have a CoordinatorLayout, your mileage may vary.
This entire feature in Android seems to be an unholy mess.
a) you can use CoordinatorLayout as your root view inside fragment
or
b) you can create custom linear layout what will call requestApplyInsets and use it as your root view inside fragment
class WindowInsetsLinearLayout : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
ViewCompat.requestApplyInsets(this)
}
}
and then inside fragment you can catch applying insets
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(root_layout) { _, insets ->
//appbar.setPadding(insets.systemWindowInsetLeft, insets.systemWindowInsetTop, 0, 0)
insets.consumeSystemWindowInsets()
}
}

Android: Remove only bottom FadingEdge effect from scroll bar

I know how to disable fadingedge from scrollbar but what I need is to disable just the bottom fading edge without disabling the top fading edge effect, is that possible?
You can achieve the effect you want by extending the ScrollView and overriding one of those two methods:
float getTopFadingEdgeStrength()
float getBottomFadingEdgeStrength()
They'll alow you to change the size of the fading edge - just set bottom value to 0 and you are ready to go :)
Code example with bottom fading turned off:
/**
* Created by scana on 14.12.14.
*/
public class TopFadeEdgeScrollView extends ScrollView {
public TopFadeEdgeScrollView(Context context) {
super(context);
}
public TopFadeEdgeScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TopFadeEdgeScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected float getBottomFadingEdgeStrength() {
return 0.0f;
}
}
scana's answer is correct.
Here's a Kotlin version of his answer that has methods to disable specific edges.
import android.content.Context
import android.util.AttributeSet
import android.widget.ScrollView
class FadingEdgeScrollView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : ScrollView(context, attrs, defStyle) {
var topFadingStrength: Float? = null
var bottomFadingStrength: Float? = null
var leftFadingStrength: Float? = null
var rightFadingStrength: Float? = null
override fun getTopFadingEdgeStrength(): Float {
return topFadingStrength ?: super.getTopFadingEdgeStrength()
}
override fun getBottomFadingEdgeStrength(): Float {
return bottomFadingStrength ?: super.getBottomFadingEdgeStrength()
}
override fun getLeftFadingEdgeStrength(): Float {
return leftFadingStrength ?: super.getLeftFadingEdgeStrength()
}
override fun getRightFadingEdgeStrength(): Float {
return rightFadingStrength ?: super.getRightFadingEdgeStrength()
}
fun disableTopFade() {
topFadingStrength = 0f
}
fun disableBottomFade() {
bottomFadingStrength = 0f
}
fun disableLeftFade() {
leftFadingStrength = 0f
}
fun disableRightFade() {
rightFadingStrength = 0f
}
}

Android TimePicker (Wheel Style) not responding correctly to flick gestures inside ScrollView

I have a dialog box that contains a Scrollview, which contains a layout with two TimePickers.
The timepickers are the newer style ones, what's in ICS.
The problem is that they seem to fight for focus when you change the time by dragging the wheel, or flicking it. It will change the time just a little, and then the layout will scroll instead.
Any ideas?
Thanks in advance.
I had the same problem when using the Holo theme, and here is where I found the solution: https://groups.google.com/forum/?fromgroups#!topic/android-developers/FkSfJI6dH8w
You must implement your custom DatePicker or TimePicker and override the following method:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN)
{
ViewParent p = getParent();
if (p != null)
p.requestDisallowInterceptTouchEvent(true);
}
return false;
}
Because the link from Klemens Zleptnig is broken, here is a complete example. This fix helps with the scroll of a TabLayout too btw. I excluded the area around the big numbers in the top of the TimePicker because they don't need the scroll event anyway.
xml:
<com.name.app.MyTimePicker
android:id="#+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
.../>
java:
public class MyTimePicker extends TimePicker {
public MyTimePicker(Context context) {
super(context);
}
//This is the important constructor
public MyTimePicker(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyTimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyTimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
//Excluding the top of the view
if(ev.getY() < getHeight()/3.3F)
return false;
ViewParent p = getParent();
if (p != null)
p.requestDisallowInterceptTouchEvent(true);
}
return false;
}
}

Categories

Resources