Restrict user scrolling in RecyclerView - android

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.

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.

RecyclerView smoothScroll to position in the center. android

I am using a horizontal layout manager for my RecyclerView.
I need to make RecyclerView in the next way: when click on some item - make smoothScrool to that position and put that item in the center of RecyclerView (if it possible, for example, 10 item from 20).
So, I have no problem with smoothScrollToPosition(), but how to put item than in the center of RecyclerView???
Thanks!
Yes it's possible.
By implementing RecyclerView.SmoothScroller's method onTargetFound(View, State, Action).
/**
* Called when the target position is laid out. This is the last callback SmoothScroller
* will receive and it should update the provided {#link Action} to define the scroll
* details towards the target view.
* #param targetView The view element which render the target position.
* #param state Transient state of RecyclerView
* #param action Action instance that you should update to define final scroll action
* towards the targetView
*/
abstract protected void onTargetFound(View targetView, State state, Action action);
Specifically in LinearLayoutManager with LinearSmoothScroller:
public class CenterLayoutManager extends LinearLayoutManager {
public CenterLayoutManager(Context context) {
super(context);
}
public CenterLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public CenterLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
RecyclerView.SmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private static class CenterSmoothScroller extends LinearSmoothScroller {
CenterSmoothScroller(Context context) {
super(context);
}
#Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
}
}
}
Improvements to the answer - there is no need to override the LinearLayoutManager
From the previous answer:
public class CenterSmoothScroller extends LinearSmoothScroller {
public CenterSmoothScroller(Context context) {
super(context);
}
#Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
}
}
Here how to use it:
RecyclerView.LayoutManager lm = new GridLayoutManager(...): // or whatever layout manager you need
...
RecyclerView.SmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
lm.startSmoothScroll(smoothScroller);
Just in case someone needs the Kotlin equivalent of the class in the accepted answer.
class CenterLayoutManager : LinearLayoutManager {
constructor(context: Context) : super(context)
constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State, position: Int) {
val centerSmoothScroller = CenterSmoothScroller(recyclerView.context)
centerSmoothScroller.targetPosition = position
startSmoothScroll(centerSmoothScroller)
}
private class CenterSmoothScroller(context: Context) : LinearSmoothScroller(context) {
override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
}
Based on #Boda's answer, if you want to control smooth scroll speed (for better animation) you can use below:
class CenterLayoutManager : LinearLayoutManager {
constructor(context: Context) : super(context)
constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(
context,
orientation,
reverseLayout
)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
)
override fun smoothScrollToPosition(
recyclerView: RecyclerView,
state: RecyclerView.State,
position: Int
) {
val centerSmoothScroller = CenterSmoothScroller(recyclerView.context)
centerSmoothScroller.targetPosition = position
startSmoothScroll(centerSmoothScroller)
}
private class CenterSmoothScroller(context: Context) : LinearSmoothScroller(context) {
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
companion object {
// This number controls the speed of smooth scroll
private const val MILLISECONDS_PER_INCH = 150f
}
}
Usage:
recyclerView.layoutManager = CenterLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
recyclerView.smoothScrollToPosition(selectedPosition)
since now(Feb 2019), I could easily use this code in ListView
(ListView)word_list_view.smoothScrollToPositionFromTop(your_item_index, center_position.y);
RecyclerView not verified, I guess would be the same.

Make fragment clickable when navigation drawer is opened

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)
}
}
}

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
}
}

How do I completely prevent a TextView from scrolling?

I have a TextView with a lot of text. This TextView has maxLines set, so it only shows the first 8 or so lines. I also have a "Read More" button so I handle expanding the TextView on my own.
My problem is that sometimes the TextView scrolls a little (just half a line at a time), even though I never specified any scroll bars. This issue is made worse because the TextView is inside a ListView, so when the user scrolls the main ListView, the TextView sometimes scrolls a little, like this:
How do I prevent the TextView from scrolling?
I have the same problem,My solution is create a NoScrollTextView extends TextView like this
public class NoScrollTextView extends TextView {
public NoScrollTextView(Context context) {
super(context);
}
public NoScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public NoScrollTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void scrollTo(int x, int y) {
//do nothing
}
}
set scrollTo do nothing
In Kotlin:
class NonScrollingTextView : TextView {
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
}
override fun scrollTo(x: Int, y: Int) {
//do nothing
}
}
So I did a little research and I don't think it's as simple as just disabling scrolling, but there are a few things you can do/try.
The first is setEnabled(false) but this will disable links and alter the text color.
The second, which I suggest trying, is using the scrollTo(int x, int y) method. Just scrollTo(0,0) after setting the text of the TextView, my guess is the large text is the only thing causing the scrolling so this should be able to take care of it.
The third answer I found that you can try is a bit more complicated and not exactly your question but it may work for you can be found here.
public class LinkMovementMethodOverride implements View.OnTouchListener{
#Override
public boolean onTouch(View v, MotionEvent event) {
TextView widget = (TextView) v;
Object text = widget.getText();
if (text instanceof Spanned) {
Spanned buffer = (Spanned) text;
int action = event.getAction();
if (action == MotionEvent.ACTION_UP
|| action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off,
ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
// Selection only works on Spannable text. In our case setSelection doesn't work on spanned text
//Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]));
}
return true;
}
}
}
return false;
}
}
"After that apply it to the target textview as touch listener: -
textview.setOnTouchListener(new LinkMovementMethodOverride());"
I needed feature like see more in my project and I used ellipsizing textview from this SO post. It works like a charm and also provides an interface for checking if the text was ellipsized. This should do the trick.
Just use setMinLines() to always display whole text
Try these lines of code in xml
android:isScrollContainer="false"
android:ellipsize="end"

Categories

Resources