How to find a View in a BridgeContext? - android

TL;DR: Is there anything in com.android.layoutlib.bridge.android.BridgeContext that can substitute for Activity#findViewById(...)? I've looked at the source, but I can't find anything.
When running on a real device, an attached view's #getContext() returns the Activity. The view can cast it and call #findViewById(...) to obtain a reference to some other view.
But when running in a WYSIWYG editor, #getContext() returns an instance of a different class. I'm getting com.android.layoutlib.bridge.android.BridgeContext. This class isn't part of the public API, so I'm planning to access it via reflection and degrade gracefully if the implementation changes.
If you're wondering why my view wants a reference to another view... I've created a view that appears to have a hole in it. It works by delegating its drawing to another view. If the view with the hole is placed on top of other views, then it appears to punch a hole through any views beneath it, all the way down to the view it's using for drawing. It works perfectly on a real device, but it would be nice to have it also work in the WYSIWYG editor.

It's bad to assume that View.getContext(), or any other platform method that returns Context, can be cast directly to more concrete classes, like Activity. There exist classes like ContextThemeWrapper which can easily destroy your assumption.
I would recommend restructuring what you are doing so that you have a parent layout that can act as an intermediary for the hole-y View and what's below it.
Or you could have a setter which would provide the View for you.
A last option is to call View.getParent() a bunch of times to get the root View and call findViewById() on that:
ViewParent parent;
while(getParent() != null) {
parent = getParent();
}
View root = (View) parent;
root.findViewById(R.id.my_view);
BTW, BridgeContext is used in the WYSIWYG in place of Activity because it only mocks the Android View/Layout/Rendering system, it doesn't emulate it completely. This can be seen in other ways like how it renders shadows or shape drawable rounded corners.

I awarded the bounty to dandc87 because his answer led me to the solution. However, the code snippet in his answer crashes with a ClassCastException because the root ViewParent is not a View. The mods keep rejecting my edits, so here's the complete and correct solution:
private View findPeerById(int resId) {
View root = this;
while(root.getParent() instanceof View) {
root = (View) root.getParent();
}
return root.findViewById(resId);
}

Related

Can a View (not ViewGroup) contain multiple UI components that each need Talkback support

Bacground
I have been working on stripping out a library that deals with adding Accessibility with Talkback that I have created in an existing app. Originally my custom views were all ViewGroups, so I got everything working amazingly with ViewGroups (focusable navigation with D-pad, initial view focus, and content descriptions)
When I was moving this to a standalone library, I noticed that it didn't work with View. I thought ViewGroup was the superclass, but it turns out that View is the superclass. So I have been trying to find some workarounds to fix my issue. I started to do the following, and have a question based on this approach...
Code In Question
public class Accessibility {
public static ViewGroupAccessibility with(ViewGroup viewGroup) {
return new ViewGroupAccessibility(viewGroup);
}
public static ViewAccessibility with(View view){
return new ViewAccessibility(view);
}
}
I have fully implemented ViewGroupAccessibility and I intend to fully implement ViewAccessibility as it is a stub right now. So far the below code works well with TalkBack, I can do ViewGroup related stuff with ViewGroups, and it appears that I can do View related stuff with Views; however, I am wondering if this is even needed
What I know
Accessibility.with(new RelativeLayout(...)) // Returns ViewGroupAccessibility as RelativeLayout is a ViewGroup
//
...will return a ViewGroupAccessibility that can handle ViewGroup related stuff that can contain many different View and ViewGroup. (See code at the bottom of this post for real usage, and what what methods are available for ViewGroupAccessibility)
Accessibility.with(new Button(...)) // Returns ViewAccessibility as Button is a View
//
...will return a ViewAccessibility that can handle single View only related stuff (that is my assumption). Think only a Button.
What I don't know
// Hypothetical Usage
Accessibility
.with(new ClassThatExtendsView_WithMultipleComponentsThatCanHaveAccessibilitySetOnEachComponentIndividually(...));
// Custom View that extends View
public class ClassThatExtendsView_WithMultipleComponentsThatCanHaveAccessibilitySetOnEachComponentIndividually extends View {
...
}
Is this even possible? If no, then I am good. If yes, then I have a lot extra to think about
It will return a ViewAccessibility that can handle single View only, but then that would be the wrong thing to return.
Another way of asking the question is am I guaranteed that if a user calls Accessibility.with(View) that the given view will ALWAYS be a single view only? Like Just a single Button. Or can the View be made of more than one component
Full Code
You can check out the code at https://codereview.stackexchange.com/questions/134289/easily-add-accessibility-to-your-app-as-an-afterthought-yes-as-an-afterthought (there is also a GitHub link to the original code). I go in incredible detail into how the project was started, my design decisions, and my future goals all to help guide the code review process.
However, here is a snippet of a usage I have for ViewGroup
public class ContributionView extends RelativeLayout implements Mappable<Resume.Contribution> {
// Called from Constructors
private void init(AttributeSet attrs, int defStyle) {
root = (ViewGroup) LayoutInflater.from(getContext()).inflate(
R.layout.internal_contribution_view, this, true);
...
// Declare Navigation Accessibility
Accessibility.with(root)
// Disable certain views in the ViewGroup from ever gaining focus
.disableFocusableNavigationOn(
R.id.contribution_textview_creator,
R.id.contribution_textview_year)
// For all focusable views in the ViewGroup, set the D-pad Navigation
.setFocusableNavigationOn(txtProjectName)
.down(R.id.contribution_textview_description).complete()
.setFocusableNavigationOn(txtContributionDescription)
.up(R.id.contribution_textview_name)
.down(R.id.contribution_textview_link).complete()
.setFocusableNavigationOn(txtProjectLink)
.up(R.id.project_textview_description).complete()
// Set which view in the ViewGroup will have be first to be focused
.requestFocusOn(R.id.contribution_textview_name);
invalidateView();
}
private void invalidateView() {
...
// Declare Content Description Accessibility
Accessibility.with(root)
// Set the content descriptions for each focusable view in the ViewGroup
// Set the content description for the Contribution Name
.setAccessibilityTextOn(txtProjectName)
.setModifiableContentDescription(getProjectName())
.prepend("Contribution occurred on the Project called ")
.append(String.format(" by %s in %s",
getProjectCreator(),
getContributionYear())).complete()
// Set the content description for the Contribution Description
.setAccessibilityTextOn(txtContributionDescription)
.setModifiableContentDescription(getContributionDescription())
.prepend("Description: ").complete()
// Set the content description for the Contribution URL
.setAccessibilityTextOn(txtProjectLink)
.setModifiableContentDescription(getProjectLink())
.prepend("URL is ").complete();
}
...
}
Yes, there is a way to move accessibility amongst the various areas/components of a View. It requires a little work, though.
Start here:
https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeProvider.html

Passing values to custom view in android

Cheers,
I have an app that receives user input (2 numbers, width and height) and in theory depending on that input I have a custom view that should draw a grid (width and height).
Note:
These 2 values should be received before view attempts to draw itself.
These 2 values aren't constant and therefore I don't think XML approach can help.
I was told that adding another parameter to the View constructor is evil.
Do not confuse my 2 values with canvas.getWidth or etc.. these are values needed simply to draw something, nothing else.
My View is also a ViewGroup.
Main issue arises with Views declared in XML files.
I have temporarily solved this issue by making an SchemeContext class which contains those 2 static values and I simply set them in onCreate (before onCreateView) then use them in custom View onDraw when needed (SchemeContext.width). This is not really what people would call OOP I'm forcing global variables upon java and those are set on time because of the fragment lifecycle.
I've seen this answer How to pass variables to custom View before onDraw() is called?.
But it's more of a workaround than a solution (and probably not the fastest one). There has to be a sensible solution I don't think 3D games on android resort to these workarounds (SurfaceView with OpenGL is still a View right? :d).
If there is an obvious solution and this is an obvious double I'll remove the question.
I haven't tried this, but I think it would be possible to do this fairly cleanly by overriding the LayoutInflater.Factory. That way, you can intercept the creation of the views that need additional parameters passed to their constructors, and let the rest of them fall through to default inflation.
For example, in your activity, before you inflate the view hierarchy:
LayoutInflater inflater = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE);
MyInflaterFactory factory = new MyInflaterFactory();
// Pass information needed for custom view inflation to factory.
factory.setCustomValue(42);
inflater.setFactory(factory);
For your implementation of the factory:
class MyInflaterFactory implements LayoutInflater.Factory {
public void setCustomValue(int val) {
mCustomVal = val;
}
#Override
public View onCreateView (String name, Context context, AttributeSet attrs) {
if (name.equals("com.package.ViewWithCustomCreation")) {
return new ViewWithCustomCreation(context, attrs, mCustomVal);
}
return null;
}
private int mCustomVal;
}
I was told that adding another parameter to the View constructor is evil.
Nonsense.
There are three (and in the newest APIs, four) different View constructors, each used in a different situation. (See this thread.) If you wanted to be able to declare your view in XML, for example, then you'd have to provide a constructor with exactly the right parameters, and have it call the corresponding superclass constructor. But there's nothing wrong with defining your own constructor (or even several of them) that call the superclass constructor intended for creating views programmatically.
The overriding principle is that every object must be valid when its constructor returns. So unless you can provide reasonable default values in your constructor, you have little choice but to accept the object's properties as constructor parameters.

Show views currently focused (Even without layout selectors)

I'm trying to inspect a code for a very big Android (Amazon Fire TV) activity but i keep loosing the focus in the running app and i don't know what element is being focused.
I'm looking for a way (Wether it's an App, a developer setting - Show Layout Limits gets near - or something i can code inside the activity) to see what view is being focused, without having to change the layout (Selectors) of every single view.
What do you suggest?
Activity has a method called getCurrentFocus().
Maybe you could call hasFocus() on all the Views if the above doesn't work. I imagine the method would look something like this:
public View getFocusedView(View layout)
{
View focusedView = null;
// Note: I'm not sure if FOCUS_DOWN is the right one to use here
// so you may want to see the other constants offered
ArrayList<View> views = layout.getFocusables(View.FOCUS_DOWN)
for(View v: views)
{
if(v.hasFocus())
{
focusedView = v;
}
}
return focusedView;
}

How to extend a complex Android view like the GridView?

Background:
I'm looking for a way to extend the GridView I need implement a col- and row-span like in HTML. I have hunderds of elements so I cannot use e.g. a TableLayout. However that would be too localized. So far I reduce the question to how to extend the GridView?
What I have tried and where I have failed:
My first approach was to look which methods I need to replace and I found some private methods which cannot be overriden. So I tried to copy the hole source code from the GridView to a new class called CustomGrid. That was a terrible failure since I cannot access the com.android.internal.R class.
Then I dropped that idea and looked if I can normal extend the class and replace all the private methods with custom copies. I took a pen and build a huge tree where all methods are used.
After I found all references I tried to extend the class normal and I added the first method:
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
// ...
Here is my next problem that member variables mBottom and mTop are unknown. I digged a little through the sources and found them finally in the View class, but unfortunately they are hidden.
/**
* The distance in pixels from the top edge of this view's parent
* to the bottom edge of this view.
* {#hide}
*/
#ViewDebug.ExportedProperty(category = "layout")
protected int mBottom;
Question:
How can I extend the GridView without hitting that lamentations and without the usage of massive reflection? I mean it seems to be impossible to write that as a pure custom control.
How can I extend the GridView without hiting that limentations and without the usage of massive reflection?
Most likely, you don't. You copy the code into your project and modify to suit, including all requisite superclasses (up to ViewGroup). I will be stunned if you can achieve your aims by a simple subclass of GridView. You may even have to completely write your desired widget from scratch.
That was a terrible failior due I cannot access the com.android.internal.R class.
You will also need to copy over relevant resources, then fix up R references to point to your own resources.
but unforcantly they are hidden.
You find other ways of getting this data. mBottom, for example, can be changed to getBottom().

Does Android API have something like Flash's duplicate movieclip?

I used to do alot of Flash Actionscript and now I am getting into Android. Is there something in the Android API that is similar to duplicateMovieClip() in Actionscript? I'm sure there is probably a way to write such a method, but I am wondering if there are any existing shortcuts.
For example, say I have an ImageView, TextView, or other kind of View Object on screen and I want to have a button to click which will make a duplicate of some object on screen.
If you don't mind my asking, why do you need something like duplicateMovieClip()?
To answer the question, Android doesn't have a notion of the AS2 duplicateMovieClip(). Much like in AS3 (which also didn't have duplicateMovieClip()) you'll have to implement your own cloning method. Java does have an unimplemented '.clone()' method as part of every Java object, so if there's a particular View you would like to clone you might be able to implement your cloning there by
Overriding the clone method.
I think what you'd probably end up doing instead is doing something more akin to instantiating from the Library by making small view layouts in xml and inflating them using the Inflater tools.
View result = null;
// where pContext is a context object, either supplied by the application
// or just by the current Activity (if available)
LayoutInflater inflater = (LayoutInflater) pContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// where id is the layout id such as R.layout.myclonableview.
// where pRoot is the parent container for the new result.
// where pAttachToRoot is whether to immediately inflate the new view into the root.
result = inflater.inflate(id, pRoot, pAttachToRoot);
// Now "clone" your old view by copying relevant fields from the old one to the
// one stored in result

Categories

Resources