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().
Related
I'm creating an app that has screens that present data to the user.
Each Screen has its own data and its own layout, so it has a method to return an int that represent the layout that is used to inflate it, then this View is passed to a function to find the specific views and populate it with data.
The lifecycle goes like this:
MainPresenter:
screen.getNextScreen ->
screen.getLayout ->
view = inflateScreen ->
screen.populateScreen(view) ->
(wait for time elappsed or click) -> repeat
Those Screens are also needed in SettingsActivity to enable\disable them.
So i've created a singleton ScreenProvider, it initializes once and then returns the list.
public class ScreenProvider {
private List<Screen> screens;
private static ScreenProvider instance = new ScreenProvider();
public static ScreenProvider getInstance(){
return instance;
}
private ScreenProvider() {
screens = new ArrayList<>();
screens.add(new Welcome());
screens.add(new CompoundScreen());
screens.add(new Times());
screens.add(new Messages());
screens.add(new Weekly());
}
public List<Screen> getScreenList() {
return Lists.newArrayList(screens);
}
}
Its seam that when running to long the app crashes or get closed for memory leaks, so i've added leakcanary, and this an example for its report:
MainActivity has leaked:
D: * static ScreenProvider.!(instance)!
D: * ↳ ScreenProvider.!(screens)!
D: * ↳ ArrayList.!(array)!
D: * ↳ array Object[].!([0])!
D: * ↳ CompoundScreen.!(disposable)!
D: * ↳ LambdaObserver.!(onNext)!
D: * ↳ -$$Lambda$Screen$67KdQ1jl3VSjSvoRred5JqLGY5Q.!(f$1)!
D: * ↳ AppCompatTextView.mContext
D: * ↳ MainActivity
This is just a single example, but almost every screen has such leak.
The LeakCanary report shows that TextView has this: D: | mAttachInfo = null so i assume it is not the problem.
Also every Screen has an onHide() to clear disposables, that is called when current Screen hides and in MainActivity.onStop().
How to fix this leak?
Should i not use a singleton for the screens?
If not, how do i access the screen list from other activities?
** Editing **
Adding some of Screen main methods that every screen overrides.
public abstract int getLayout();
public boolean shouldShow()
public void populateData(View view)
public void onHide()
public abstract int getScreenIndex();
public boolean shouldCacheView()
public int getDuration()
Okay. From what you are saying, and what you are showing, it seems you are keeping instances of some of the generated views into the Singleton. Don't. Every View requires a Context to be created, either by code, or by inflation (which is basically an XML-backed, reflection-based factory method), to access the app and system's resources, and keep a reference to said Context, for as long as they live. In your scenario, that means keeping references to the activity where you generated the view. Usually, regarding views and activities, this is what happens regarding the GC:
GC: Hey! Does anybody need this... MainActivity class?
View: I do! I do! I have a reference!
GC: Okay... and besides MainActivity, Does anybody else need this View class?
-Nobody answers-
GC: It does not matter my friend, you are being collected as well. Come with me.
And they both go.
In your case:
GC: Hey! Does anybody need this... MainActivity class?
View: I do! I do! I have a reference! and MainActivity references me as well.
GC: Okay... and besides MainActivity, Does anybody else need this View class?
ScreenProvider: I do.
GC: Okay, keep moving View, and take MainActivity with you. Let me know when you folks are done so I can collect you.
And thus the leak.
In order to pass a view from one activity to another, you would need to remove the reference (the mContext field) to the previous activity. Since there's no API for doing that, you would need to use reflection. And there arises another problem: Every UI piece is a subclass of View. Layouts, Widgets, etc, etc. So either you keep a reference to every piece of your XML file in order to remove the context via reflection, or you traverse the child list of the view, remove the context, and keep going until there are no more child views, at any level. After that, you would have to set the reference to the new activity in the same way. It sounds like a huge hack, because it is, and things are bound to break at some level. A Context after all represents the environment and state within your view exist.
A better solution for your situation is to remove the view references from the singleton, and use it only to keep representations of the state/configuration of a given view. Create a callback backed method (or similar) that inflates the view in background and performs the necessary configurations before returning said view. If you still want to keep a single repository of all the Screens the activity may have, add it to the activity class as a member, so it gets collected alongside the activity.
As a side note, your situation suggests you should use a single activity, and then just swap a "MainScreen" composed of "Screens", or just switch between screens depending on the situation. It would make more sense and would be less risky.
Finally, citing myself: Remember the first rule of the android fight club
I'm writing an application for iOS and Android in parallell and I am facing a small problem.
I am displaying a list of settings to the user and the settings data is internally represented in settings for section, like this:
Section
Section object
Section object
Section
Section object
etc.
In iOS, when the user clicks a setting object, or when the system wants to paint the view for it, it calls a method with an NSIndexPath object. For example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
and the sections are handled automatically.
In Android, the listviews are "flattened", lacking better terminology. So that a method is called only with a row index:
public Object getItem(int position);
Now it is actually a quite hard problem to solve trying to represet sections directly in the Adapter (MySettingsAdapter extends BaseAdapter).
Right now this problem is solved by flattening the entire underlaying data structure, but it's a lot of duplicated code for almost nothing. The nicer solution I can think of is to do something like the following in my MySettingsAdapter:
class MySettingsAdapter extends BaseAdapter {
MyInternalDataStructure settingsData;
int sections;
int rowsForSection[sections];
public MySettingsAdapter (MyInternalDataStructure settingsData) {
this.settingsData = settingsData;
this.sections = settingsData.sections;
for (int i = 0; i < sections; i++) {
rowsForSection[i] = settingsData.settingsInSection[i].size();
}
}
#Override
public Object getItem(int position) {
int sectionFromPostion;
int rowFromPosition;
// Calculate section and row here...
return settingsData.getSetting(sectionFromPostion, rowFromPosition);
}
}
And I just can't get the calculations for sectionFromPostion and rowFromPostion right...
Unfortunately Android does not have quite the same ability in regards to sections as you can achieve with the UITableView. However Android does provide a solution, the ExpandableListView. While similar to a ListView it works a bit differently and interacts with a different type of adapter.
Android provides the SimpleExpandableListAdapter that you can use with the ExpandableListView. I'll warn you now. It's clunky and pretty restricting. Additionally, it requires you to organize your data into a List of Maps which in itself can be a pain to do.
Alternatively, you can create your own adapter for the ExpandableListView by implementing the BaseExpandableListAdapter. It's very similar to implementing the BaseAdapter. It just has a few extra bells and whistles to support a tier like structure.
Basically all these Expandable...[foo] classes introduce the idea of having a group (the section) and children (the data under a section). Instead of having an index to the data in your adapter, you'll have a groupPosition and a childPosition. Meanwhile the ExpandableListView has this sorta murky middle notion of positions as it works with group/child positions, packed positions, and flattened positions.
As a side note. Depending on how your data is organized, I'd suggest checking out the Rolodex Adapters found in this 3rd party library. They are meant to make working with ExpandableListViews easier and has plenty of code examples and a demo app to help get you going.
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.
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);
}
Libreoffice (and other apps like Latex) allows you to use some kind of script language to show really cool mathematical formulas.
I need to use a view (probably a textView) that will be able to shows such formulas, and fractions in particular.
For example "x over y" would look like:
x
-
y
and so on.
Is there any such solution for this on Android? Maybe a library (with license similar to Apache or better) that allows you to write such things?
You could easily extend the view class and replicate this with two TextViews and an ImageView. Probably not the most efficient way of doing this but certainly pretty easy.
Some sudo code but you get the idea..
public class FractionView extends View {
private TextView top;
private Textview bottom;
private ImageView line;
public FractionView(Context context) {
// instantiate the views
}
// TODO add methods to update the text, possibly update positions as well, more stuff?
}