Loop focus in RecylerView for Tv - android

according to https://developer.android.com/training/tv/start/navigation
You should set up the navigation order as a loop, so that the last
control directs focus back to the first one.
I have tried to implement such behavior for RecyclerView items:
fun RecyclerView.loopFocusVertically() {
viewTreeObserver.addOnGlobalLayoutListener {
val children = getAllChildren()
val firstFocusableItem = children.firstOrNull { it.isFocusable }
val lastFocusableItem = children.lastOrNull { it.isFocusable }
firstFocusableItem?.let { it.nextFocusUpId = lastFocusableItem?.id ?: it.id }
lastFocusableItem?.let { it.nextFocusDownId = firstFocusableItem?.id ?: it.id }
}
}
So on layout change i made first view have nextFocusUpId reference to last view, and the last view nextFocusDownId reference to first view. But it only works in case all views are actually layouted on screen, which obviously is not true in general case. RecyclerView actually recycles views, so last and first view may not exist at the same time on screen
How to implement focus loop for rv in leanback mode?
I have tried to use androidx.leanback.widget.VerticalGridView from leanback library, but it also doesn't have required behavior.
By default on pressing up from first focusable item of rv focus jumps to some other focusable view outside of rv, which is not so appropriate

By default on pressing up from first focusable item of rv focus jumps to some other focusable view outside of rv, which is not so appropriate
First, as for the UX, it's really problematic if you have views above your list so pressing up will really be an issue if to scroll to the last item as you want or to go to the above view.
With that said I'd suggest you give the loop option only from bottom in this case if you have a view above the list that should also get focus.
In general, I suggest you check VerticalGridView/HorizontalGridView for adding rows and lists in a lean-back app.
In the example I adjusted it to use a RecyclerView for your question.
For doing so you'll have to catch the button event, check if its the last item and if so, request focus to the first item.
#Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) {
if (onDownClick(event)) {
return true;
}
}
}
return super.dispatchKeyEvent(ev);
}
public boolean onDownClick(KeyEvent event) {
int currentIndex = 0;
if (mAdapter != null) {
currentIndex = mAdapter.indexOf(mCurrentItem);
}
if (currentIndex == mAdapter.size() - 1) {
mListRecycler.scrollToPosition(0);
if (mListRecycler.findViewHolderForAdapterPosition(0) != null) {
mListRecycler.findViewHolderForAdapterPosition(0).itemView.requestFocus();
}
return true;
}
return false;
}

Related

HorizontalScrollView with EditText's scrolls when keyboard appear

I have successfully created a list (LinearLayout) that contains multiple dynamic elements/rows. It is filled with content received by webservice.
One of the elements per row is a HorizontalScrollView that contains a variable amount of EditText fields.
That means that all edittexts from all rows (including a header) can scroll with that horizontalScrollView.
A Scrollmanager will make sure that all horizontalScrollviews move simultaneously. So it is basically a scrollable column within a list.
The problem that i am experiencing is as follows.
When i select a EditText view it will show the keyboard, which is what i want it to do. But the scrollManager is triggered so it will scroll all horizontalscrollviews to the end. Instead of keeping the focussed edittext in screen, it will move out sight.
My ScrollManager OnScrollChanged
#Override
public void onScrollChanged(View sender, int l, int t, int oldl, int oldt) {
// avoid notifications while scroll bars are being synchronized
if (isSyncing)
return;
isSyncing = true;
// remember scroll type
if (l != oldl)
scrollType = SCROLL_HORIZONTAL;
else if (t != oldt)
scrollType = SCROLL_VERTICAL;
else {
// not sure why this should happen
isSyncing = false;
return;
}
// update clients
for (ScrollNotifier client : clients) {
View view = (View) client;
if (view == sender)
continue; // don't update sender
// scroll relevant views only
// TODO Add support for horizontal ListViews - currently weird things happen when ListView is being scrolled horizontally
if ((scrollType == SCROLL_HORIZONTAL && view instanceof HorizontalScrollView)
|| (scrollType == SCROLL_VERTICAL && view instanceof ScrollView)
|| (scrollType == SCROLL_VERTICAL && view instanceof ListView)) {
view.scrollTo(l, t);
}
}
isSyncing = false;
}
I the end i want the keyboard to appear and the scrollview to be able to scroll, but i want to prevent the horizontal scroll event when the keyboard appears.
I haven't tested this yet, but you should be able to stop propogation of the touch event to the HorizontalScrollView by setting an OnTouchListener to your EditText and overriding the OnTouch method like this:
#Override
public boolean onTouch(View v, MotionEvent event)
{
Log.i("OnTouch", "Fired On Touch in " + v.getId());
// Do this on the down event too so it's not getting fired before up event
if(event.getAction() == MotionEvent.ACTION_DOWN)
{
// Disallow ScrollView to intercept touch events.
v.getParent().requestDisallowInterceptTouchEvent(true);
}
//only do this on the up event so you're not doing it for down too
if(event.getAction() == MotionEvent.ACTION_UP)
{
// Disallow ScrollView to intercept touch events.
v.getParent().requestDisallowInterceptTouchEvent(true);
}
//Keep going with the touch event to show the keyboard
v.onTouchEvent(event);
return true;
}

HorizontalScrollView onTouch

I Am using Horizontal Scroll View and its work fine , i have 2 web view inside my horizontal Scroll View , my problem is that when u scroll down or up in the web view sometime it go left or right to the other web view because of the Horizontal Scroll View , i use this code
HorizontalScrollView hv = (HorizontalScrollView)findViewById(R.id.horizontalScrollView2);
webView22.setOnTouchListener(new View.OnTouchListener() {
private String TAG;
public boolean onTouch(View v, MotionEvent event) {
Log.v(TAG,"PARENT TOUCH");
findViewById(R.id.webView22).getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
});
webView22.setOnTouchListener(new View.OnTouchListener() {
private String TAG;
public boolean onTouch(View v, MotionEvent event)
{
Log.v(TAG,"CHILD TOUCH");
// Disallow the touch request for parent scroll on touch of child view
findViewById(R.id.webView23).getParent().requestDisallowInterceptTouchEvent(true);
return false;
}
});
when i use the TAG i got error say The field FragmentActivity.TAG is not visible
but when i add the private String TAG; it go , anyway i am not sure if it is correct ,
How ever after i test it the webView22 is good and great but if i want to go right and left using the Horizontal it will not be work ,
i tried to change the CHILD TOUCH to BUTTON_BACK but still the same . i feel that tag not doing anything it just go to
findViewById(R.id.webView22).getParent().requestDisallowInterceptTouchEvent(false);
return false;
i hope some one can help
Actually, the case here is that both the horizontal scroll view and the web view consume the scroll event. Now it will be hard for the Android OS to decide onto which listener should this scroll event go to.
An option to this will be using Slide animations instead of a horizontal scroll view.
fragmentTransaction.setCustomAnimations(R.anim.slide_bottom_in,
R.anim.slide_bottom_out,R.anim.slide_bottom_in,
R.anim.slide_bottom_out);
But these will again need to be triggered with some event. like button click.
Or you can go for the view pager implementation.
i had the same issue, there are some ways, but many of them require changing the layout, wrong data is send to the child of HorizontalScrollView mostly in the cases where there are children inside the child of HorizontalScrollView.
So i did things like this, instead of:
val childA = horizontalScrollView.getChild(0)
childA.setOnTouchListener(this)
do
horizontalScrollView.setOnTouchListener(this)
This way you have to calculate the clicks, and calls, but it works better than before to me, the scroll was slow and glitchy before.
var touchHistory=0
var lastTouchX=0f
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
if (event == null || v == null) return false
when {
event.action == MotionEvent.ACTION_DOWN -> {
touchHistory=0
lastTouchX=event.x
// Screen1=0-480 / Screen2=0-2304 / ScrollPos=0-2304
// Sample: 240 480 - 576 2304 = 240+576 = result
val child = (v as HorizontalScrollView).getChildAt(0)
val sXCurrent = event.x.toDouble()
val sXMax = v.width.toDouble()
val s2XCurrent = scrollX
val s2XMax = child.width
val clickPos = s2XCurrent+sXCurrent
val result = clickPos/s2XMax
// Do something
}
event.action == MotionEvent.ACTION_UP -> {
if(touchHistory==0){
//Click performed, do something
}
//if touchHistory > 0 is currently scrolling and click is omitted
lastTouchX=event.x
touchHistory=0
}
event.action == MotionEvent.ACTION_MOVE -> {
if(event.x!=lastTouchX)touchHistory++ //Check wether is clicking or scrolling
lastTouchX=event.x
}
}
return false
}

How to use D-pad navigate switch between listview's row and it's decendants (Google-TV support)

I have a listview which has an imageview inside each list item, when user click on that imageview, it will pop-up a menu.
It works very well on a normal android device with a touch screen.
But now I want to support google-tv, which app should be control by D-pad.
When I use D-pad to navigate through listview, only the entire row can be focus, I can't get the imageview inside listview be focus.
I try to add android:focusable="true" to imageview, but it cause the entire listview can't receive onItemClick event.
Does anyone know how to move the focus between listview rows and item inside listview using d-pad also keeps listview and imageview clickable?
Thanks a lot!
You have to set the following for your ListView:
listView.setItemsCanFocus(true);
With that focusable views inside of your listItems won't be ignored.
I got my Listview work with D-pad which can switch focus inside a list item.
This is how I solve it.
First, let your list view is item focusable.
NOTE: If you trying to set ItemsCanFocus to false later in your code, your will find your list item can't get focus anymore even if your set back to true again, so don't do that.
mDpadListView.setItemsCanFocus(true);
Then you need a field to keep tracking which list item is current selected.
Here I put the ViewHolder in the listItem's tag in Adapter.
mDpadListView.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view,
int position, long id) {
if (view.getTag() != null) {
DpadListAdapter.ViewHolder holder = (ViewHolder) view.getTag();
if (holder.shortCut != null && holder.shortCut.isShown()) {
currentSelectView = view;
} else {
currentSelectView = null;
}
} else {
currentSelectView = null;
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
Third, override onKeyDown() in Activity Method to control up, down, left, right key for D-pad.
When user press right button on D-pad, I let the listview to clearFoucs() and let the ImageView inside to obtain focus.
When user press up, down or left, the ImageView in list item will clear it's focus and the listView obtain focus again.
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (currentSelectView != null) {
DpadListAdapter.ViewHolder holder =
(ViewHolder) currentSelectView.getTag();
mDpadListView.clearFocus();
holder.shortCut.setFocusable(true);
holder.shortCut.requestFocus();
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
if (currentSelectView != null) {
DpadListAdapter.ViewHolder holder =
(ViewHolder) currentSelectView.getTag();
if (holder.shortCut.hasFocus()) {
holder.shortCut.clearFocus();
holder.shortCut.setFocusable(false);
mDpadView.requestFocus();
return true;
}
}
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
You can use the properties to access the D-pad as below in your layout file:
android:nextFocusLeft="#+id/abc"
android:nextFocusRight="#+id/xyz"
android:nextFocusUp="#+id/pqr"
android:nextFocusDown="#+id/mno"
android:nextFocusForward="#+id/finish"
This all the properties will help you to access the D-pad in Google-TV.

Get child that last had focus

I have found that when using a d-pad or trackball to navigate in my app if you move to the right and the list view looses focus, when the list view regains focus, a different child is given focus than last had focus. I tried to use onFocusChange(...) to save which child had focus but it looks like this isn't called until after the focus is lost so I can never grab which child last had focus. Is there a way to grab who had focus so I can then call requestFocus() on the child once the list view grabs focus again?
Unfortunately I cannot use a handler because this isn't a much used feature and I don't want to sacrifice the performance for a smaller feature.
Here is the code I had that didn't work (focusedView was always null no matter what):
mainListView.setOnFocusChangeListener(new OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
if(focusedView == null ) { Log.i("focus", "focusedView == null"); }
if(hasFocus && focusedView != null) {
Log.i("focus", "Focus has been Recieved.............");
focusedView.requestFocus();
} else {
// Focus has been lost so save the id of what the user had selected
Log.i("focus", "Focus has been Lost.............");
focusedView = mainListView.findFocus();
}
}
});
Thanks!
I think you are spot on with your own answer, bascially since the focus listener is called after its lost focus, no child will be in focus and therefore .findFocus() will return null.
I recon you best bet is to extend the ListView class, and override the onFocusChanged method. Basically do what you are doing, but do it inthere when gainFocus is true.
#Override
protected void onFocusChanged (boolean gainFocus,
int direction, Rect previouslyFocusedRect) {
if(gainFocus)
focusedView = mainListView.findFocus();
}
This one

Overriding onTouchEvent competing with ScrollView

From a simplistic overview I have a custom View that contains some bitmaps the user can drag around and resize.
The way I do this is fairly standard as in I override onTouchEvent in my CustomView and check if the user is touching within an image, etc.
My problem comes when I want to place this CustomView in a ScrollView. This works, but the ScrollView and the CustomView seem to compete for MotionEvents, i.e. when I try to drag an image it either moves sluggishly or the view scrolls.
I'm thinking I may have to extend a ScrollView so I can override onInterceptTouchEvent and let it know if the user is within the bounds of an image not to try and scroll. But then because the ScrollView is higher up in the hierarchy how would I get access to the CustomView's current state?
Is there a better way?
Normally Android uses a long press to begin a drag in cases like these since it helps disambiguate when the user intends to drag an item vs. scroll the item's container. But if you have an unambiguous signal when the user begins dragging an item, try getParent().requestDisallowInterceptTouchEvent(true) from the custom view when you know the user is beginning a drag. (Docs for this method here.) This will prevent the ScrollView from intercepting touch events until the end of the current gesture.
None of the solutions found worked "out of the box" for me, probably because my custom view extends View, not ViewGroup, and thus I can't implement onInterceptTouchEvent.
Also calling getParent().requestDisallowInterceptTouchEvent(true) was throwing NPE, or doing nothing at all.
Finally this is how I solved the problem:
Inside your custom onTouchEvent call requestDisallow... when your view will take care of the event. For example:
#Override
public boolean onTouchEvent(MotionEvent event) {
Point pt = new Point( (int)event.getX(), (int)event.getY() );
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (/*this is an interesting event my View will handle*/) {
// here is the fix! now without NPE
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
clicked_on_image = true;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (clicked_on_image) {
//do stuff, drag the image or whatever
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
clicked_on_image = false;
}
return true;
}
Now my custom view works fine, handling some events and letting scrollView catch the ones we don't care about. Found the solution here: http://android-devblog.blogspot.com.es/2011/01/scrolling-inside-scrollview.html
Hope it helps.
There is an Android event called MotionEvent.ACTION_CANCEL (value = 3). All I do is override my custom control's onTouchEvent method and capture this value. If I detect this condition then I respond accordingly.
Here is some code:
#Override
public boolean onTouchEvent(MotionEvent event) {
if(isTouchable) {
int maskedAction = event.getActionMasked();
if (maskedAction == MotionEvent.ACTION_DOWN) {
this.setTextColor(resources.getColor(R.color.octane_orange));
initialClick = event.getX();
} else if (maskedAction == MotionEvent.ACTION_UP) {
this.setTextColor(defaultTextColor);
endingClick = event.getX();
checkIfSwipeOrClick(initialClick, endingClick, range);
} else if(maskedAction == MotionEvent.ACTION_CANCEL)
this.setTextColor(defaultTextColor);
}
return true;
}

Categories

Resources