I have created a Custom preference which has the following constructor
public CoordinatesPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
setLayoutResource(R.layout.coordinates_preference);
}
And I have Overriden onCreateView() so it writes to the log like this:
#Override
protected View onCreateView(ViewGroup parent)
{
Log.d("test", "Creating Preference view");
return super.onCreateView(parent);
}
and my log is full of "Creating Preference view" messages, this creates a laggy feel to scrolling and I believe convert view is supposed to solve this, I had a look at the preference source code and if convert view is null then onCreateView() is called.
for testing purposes I added this method:
#Override
public View getView(View convertView, ViewGroup parent)
{
if (convertView == null)
{
return super.getView(convertView, parent);
}
return super.getView(convertView, parent);
}
and set a break point. I have found that almost always my convert view is null. and therefore it must create a new view, why is this? and how can I improve this to avoid a laggy preference screen?
EDIT: Changed the way the onCreate is called, now its all android I just use setLayoutResource. but this does not solve the problem...
EDIT2: I have used Debug.StartMethodTracing() and have found as I suspected that 55% of the time spend (when I'm just scrolling up and down) is spend on the Inflation of the preference from the method onCreateView() which is called from getView() when convertView is null.
Thanks, Jason
I don't know what have you implemented in this custom preference, but maybe the super class doesn't know how create a proper view to your preference?
From the documentation:
protected View onCreateView (ViewGroup
parent)
Since:
API Level 1 Creates the View to
be shown for this Preference in the
PreferenceActivity. The default
behavior is to inflate the main layout
of this Preference (see
setLayoutResource(int). If changing
this behavior, please specify a
ViewGroup with ID widget_frame. Make
sure to call through to the
superclass's implementation.
http://developer.android.com/reference/android/preference/Preference.html
I guess you have set that id on the horizontal layout.
Now that I'm talking about it, why don't you include this horizontal layout in the layout that you are inflating?
I am not sure if the code you are using is an accurate test. I have a custom preference and I only override 5 methods and three of them are constructors.
public ImageButtonPreference(Context context)
{
this(context, null);
}
public ImageButtonPreference(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public ImageButtonPreference(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
mInflater = LayoutInflater.from(context);
// This is where I pull all of the styleable info from the attrs
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ImageButtonPreference, defStyle, 0);
for(int i = a.getIndexCount(); i >= 0; i--)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.ImageButtonPreference_logo:
mImageResource = a.getResourceId(attr, mImageResource);
break;
case R.styleable.ImageButtonPreference_button_text:
mButtonText = a.getString(attr);
break;
}
}
}
#Override
protected View onCreateView(ViewGroup parent)
{
View view = mInflater.inflate(R.layout.image_button_preference, parent, false);
return view;
}
#Override
protected void onBindView(View view)
{
super.onBindView(view);
ImageView image = (ImageView)view.findViewById(R.id.Logo);
if(image != null && mImageResource != 0) image.setImageResource(mImageResource);
Button button = (Button)view.findViewById(R.id.ConnectButton);
if(button != null)
{
button.setText(mButtonText);
button.setOnClickListener(mButtonListener);
}
}
I have pulled this code, almost verbatim from the Android Source, so it should be just as fast as any other preference.
I was running into this problem and I tracked it down to the fact that I had the layout set both in the preferences.xml file and in my Preference subclass onCreateView() method. When I removed the layout from preferences.xml, onCreateView() stopped getting called multiple times.
As a more general answer, not specifically relating to custom preferences:
It's hard to see with the code you posted, but if your needs to pull a preference every time it creates a view, it will be VERY slow and laggy as you describe. Even if the view does exist, you still need to set the value, and it sounds like that needs to come from the preference. Android preference reads are incredibly slow, so they can't be associated with UI creation if you want a good fast experience.
I think you should store the preferences in the app (perhaps in the Activity, or subclass the Application and store them in there), in order to implement some simple caching. i.e. the first time you need a preference, request it from your app's store, if it's not there, pull it out of the preferences. If the preference is stored in the activity/application already, use that value without reaching out to the prefs. Then, when you write prefs out, write to the store AND the preference. By doing that it doesn't matter how often getView() needs to create new views, as the preference can be accessed quickly using the copy in the activity/application object, but also stored durably in the preferences for the future.
I don't know whether the preferences framework has some caching in it somewhere, but my experience of loading prefs is that if a few need loading the user WILL notice a lag, so caching is essential.
Related
I have a view that inherits from ConstraintLayout. Inside this layout I place the children by use of a ConstraintSet.
This works as long as use a given AttributeSet form outside in the constructor:
public AnswerView(Context context, AttributeSet attrs) {
super(context, attrs);
It does not work, if I try to assign the layout otherwise, when I don't have attrs available.
public AnswerView(Context context) {
super(context);
setLayoutParams(new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
In the first case the children get a pretty distribution like defined by the ConstraintSet, in the second case the all line up at the left border.
In both cases the layout stretches full width, as I can prove by setting a background color.
What is missing in my code?
This is not a direct answer to the question. A direct answer is still missing. This answer is of the type, "take a better approach!".
When there are few or no answers to a question, I am usually on a track, that few people go. Then there are reasons why they don't.
My approach to programatically nest views within other views is cool, but it turns out to be difficult to do this without the use of layouts. It's too expensive to set up all the benefits of the configurations programatically, that can easily done within a layout. The API of Android is not well prepared for this.
So I turned back to the approach, to create the view classes based on layouts. This means, I create the view with the two parameter constructor for layouts.
In the enclosing view class I can't create the nested view directly any more, as there is no constructor for this. Instead I read the configured view from a layout.
I created a small class to assist extracting configured subparts of a layout:
public class Cloner {
LayoutInflater inflater;
int layoutId;
public Cloner of(Context context) {
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return this;
}
public Cloner from(#LayoutRes Integer layoutId) {
this.layoutId = layoutId;
return this;
}
public View clone(#IdRes int id) {
assert inflater != null;
View layout = inflater.inflate(layoutId, null);
View view = layout.findViewById(id);
((ViewManager) view.getParent()).removeView(view);
return view;
}
}
It is used like this:
MultipleChoiceAnswerView mcav =
(MultipleChoiceAnswerView) new Cloner().of(getContext())
.from(layoutId).clone(R.id.multipleChoiceAnswerView);
mcav.plugModel(challenge.getAnswer());
This already shows, how I connect the model in a second step, because I can't feed it into by the constructor any more.
In the constructor I first evaluate the given attributes. Then I set up the view by inflating an accompanying second layout file, that I don't show here. So there are two layouts involved, one to configure the input to the constructor, one for the internal layout.
When the call to plugModel happens, the inflated internal layout is used and extended by objects matching the given model. Again I don't create this objects programatically, but read them from a third (or the second) layout file as templates. Again done with the assistance of the Cloner given above.
private Button getButton(final Integer index, String choice) {
Button button = (Button) new Cloner().of(getContext()).from(layoutId).clone(R.id.button);
button.setId(generateViewId());
button.setText(choice);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
answer.choiceByIndex(index);
}
});
return button;
}
In practice I put this object templates (like a button) as children into the second layout file. So I can style it as a whole in Android Studio Designer.
Programatically I remove the templates before I actually fill the layout with the cloned objects. By this approach I only need to maintain one layout file per view class. (The configuration of the constructor happens in the layout file of the enclosing view.)
I have my own style for buttons defined as themes but I also use my own class to handle buttons (because of own fonts). Is it possible to call my button with a pretty name such as
<MyButton>
instead of
<com.wehavelongdomainname.android.ui.MyButton>
So the answer, surprisingly, is "yes". I learned about this recently, and it's actually something you can do to make your custom view inflation more efficient. IntelliJ still warns you that its invalid (although it will compile and run successfully) -- I'm not sure whether Eclipse warns you or not.
Anyway, so what you'll need to do is define your own subclass of LayoutInflater.Factory:
public class CustomViewFactory implements LayoutInflater.Factory {
private static CustomViewFactory mInstance;
public static CustomViewFactory getInstance () {
if (mInstance == null) {
mInstance = new CustomViewFactory();
}
return mInstance;
}
private CustomViewFactory () {}
#Override
public View onCreateView (String name, Context context, AttributeSet attrs) {
//Check if it's one of our custom classes, if so, return one using
//the Context/AttributeSet constructor
if (MyCustomView.class.getSimpleName().equals(name)) {
return new MyCustomView(context, attrs);
}
//Not one of ours; let the system handle it
return null;
}
}
Then, in whatever activity or context in which you're inflating a layout that contains these custom views, you'll need to assign your factory to the LayoutInflater for that context:
public class CustomViewActivity extends Activity {
public void onCreate (Bundle savedInstanceState) {
//Get the LayoutInflater for this Activity context
//and set the Factory to be our custom view factory
LayoutInflater.from(this).setFactory(CustomViewFactory.getInstance());
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_with_custom_view);
}
}
You can then use the simple class name in your XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<MyCustomView
android:id="#+id/my_view"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center_vertical" />
</FrameLayout>
Defining your own subclass of LayoutInflater.Factory seems a lot of work me.
Simply override the Activity's onCreateView() with some generic code:
#Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view;
// No need wasting microseconds getting the inflater every time.
// This method gets called a great many times.
// Better still define these instance variables in onCreate()
if (mInflator == null){
mInflator = LayoutInflater.from(context);
mPrefix = ((Activity) context).getComponentName().getClassName();
// Take off the package name including the last period
// and look for custom views in the same directory.
mPrefix = mPrefix.substring(0, mPrefix.lastIndexOf(".")+1);
}
// Don't bother if 'a path' is already specified.
if (name.indexOf('.') > -1) return null;
try{
view = mInflator.createView(name, mPrefix, attrs);
} catch (ClassNotFoundException e) {
view = null;
} catch (InflateException e) {
view = null;
}
// Returning null is no big deal. The super class will continue the inflation.
return view;
}
Note the custom views must reside in the same package (i.e. in the same directory) as this activity, but then it's just a generic piece of code you can slap in any activity (or even better, inherit from a custom parent activity class).
You're not worried about looking out for a particular class as specified in the solution offered by kcoppock:
if (MyCustomView.class.getSimpleName().equals(name)) {....
You're certainly not creating a whole new class.
The real magic is in the core library class, LayoutInflator.java. See the call, mPrivateFactory.onCreateView(), below?:
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
}
if (view == null) {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
You see, if the so called, mPrivateFactory, returns null (mPrivateFactory happens to be your activity class by the way), the LayoutInflator just carries on with it's other alternative approach and continues the inflation:
if (view == null) {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
It's a good idea to 'walk through' the library classes with your IDE debugger and really see how Android works. :)
Notice the code,if (-1 == name.indexOf('.')) {, is for you guys who still insist on putting in the full path with your custom views, <com.wehavelongdomainname.android.ui.MyButton> If there is a 'dot' in the name, then the creatview() is called with the prefix (the second parameter) as null: view = createView(name, null, attrs);
Why I use this approach is because I have found there were times when the package name is moved (i.e. changed) during initial development. However, unlike package name changes performed within the java code itself, the compiler does not catch such changes and discrepancies now present in any XML files. Using this approach, now it doesn't have to.
Cheers.
You can also do this:
<view
class="com.wehavelongdomainname.android.ui.MyButton"
... />
cf. http://developer.android.com/guide/topics/ui/custom-components.html#modifying
No. You need to to give "full path" to your class, otherwise framework will not be able to inflate your layout.
In order to provide a custom typeface in my ListActivity, I wrote a class CustomAdapter extending BaseAdapter according to this example here.
However, as described there, I wrote the getView() method like following:
public View getView(int position, View convertView, ViewGroup parent){
String gameName = gameNames[position]; // gameName ist the String[] of the Custom Adapter
TextView tv = new TextView(context);
tv.setText(gameName);
tv.setTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/gulim.ttf"));
return tv;
}
This works as intended. The only disturbing thing is that it takes about three or four seconds until the list shows up (and this is a very long time in this context). However, in the ListActivity I set the onItemClickListeners like this:
private void setOnItemClickListener(){
getListView().setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, int pos, long id){
onClickEntryButton(((TextView) view).getText().toString());
}
});
}
private void onClickEntryButton(String gameName){
Intent intent = new Intent(this, GameActivity.class);
intent.putExtra("gameName", gameName);
startActivity(intent);
finish();
}
Now when clicking on a ListItem, it takes even more time until the GameActivity opens. This Activity is just a couple of TextViews filled with information taken from a SQLite database. Also here, I set a custom typeface to every TextView. It happens even that the screen gets black for 2-3 seconds (appearing the app crashed), then the new Activity shows up. This doesn't happen accessing that Activity from other places in the application.
In both cases - accessing the ListActivity and accessing the GameActivity from the ListActivity - a couple of
"szipinf - Initializing inflate state"
messages appear in the LogCat. What does they mean in this context? Would it be a better usage to set the onItemClickListeners into the getView() method of my CustomAdapter? Something seems to really inhibit the transitions, but I can't figure out what, since there is nothing big to be calculated or processed (in fact, in the SQLite database are exactly two entries with each 5 fields)?
EDIT
If required or desired, of course I can provide more code.
I had exactly same issue and your question gave me answer!
I still do not know exact root cause but this the isuue is because of reading custom font from assets multiple times in my case.
If I have 10 widgets in my screen and each of them is using custom font Android loads it from assets everytime. which was not just causing my activity transions to become slow it was also resulting in crash after playing with it multiple tmes.
So I created a cache for my typeface to avoid fetching typeface everytime from assets.
I added this code inside my utility class:
private static final Hashtable<String, Typeface> cache = new Hashtable<String, Typeface>();
public static final String ASSET_PATH="assetPath";
public static Typeface getFont(Context c, String assetPath) {
synchronized (cache) {
if (!cache.containsKey(assetPath)) {
try {
Typeface t =(Typeface.createFromAsset(c.getAssets(),
"fonts/arial.ttf"));
cache.put(assetPath, t);
} catch (Exception e) {
return null;
}
}
return cache.get(assetPath);
}
}
I created my custom class to to setTypeface
public class MyButton extends Button {
public MyButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context) {
super(context);
}
#Override
public void setTypeface(Typeface tf) {
super.setTypeface(Util.getFont(getContext(), Util.ASSET_PATH));
}
}
variable assetPath can be used to provide diffrent fonts at runtime
EDIT: Here is custom typefaceManager I have created as a library to make it more generic.
I'm currently working on an app that contains a timetable screen, which is built in a highly customised way and contains a lot of 'repeated' views.
I've got each individual view that I need (eg, a view for a box that contains the title of an event and the time that it's on) set up in XML, which I inflate in a custom view class. For example:
public class EventCell extends RelativeLayout {
private TextView eventTitle;
private TextView eventTime;
private Button favouritesButton;
public EventCell(Context context) {
super(context);
setupView(context);
}
public EventCell(Context context, AttributeSet attrs) {
super(context, attrs);
setupView(context);
}
private void setupView(Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.timetable_event, this);
eventTitle = (TextView) findViewById(R.id.event_title);
eventTime = (TextView) findViewById(R.id.event_time);
favouritesButton = (Button) findViewById(R.id.favourites_button);
}
...
}
This is fine except for the fact that this view is reused quite a lot in its containing activity. Eg, it might be instantiated 50 times. My problem is that this wastes a lot of memory and causes some devices to crash.
In ListViews, there's the getView() method which gives an a convertView parameter that lets you check if the current row has already been instantiated, and then lets you update the values on that. What I'm after is a similar thing for this custom view; ideally reusing it rather than instantiating it a bunch of times.
If there isn't a way, what's the best method of getting around it? The views themselves aren't particularly complicated but still manage to bring the most devices to their knees it seems.
I am customizing an Android control. This control uses a merge layout containing an edit field (fluff left out):
<merge xmlns:android="http://schemas.android.com/apk/res/android">
...
<EditText android:id="#+id/numberpicker_input">
...
/>
...
</merge>
This works fine for a single control. However, if I put multiple instances of this control in a layout, it results in weird behaviour when rotating a device from portrait to landscape. All controls get the same value restored and callbacks get attached to the wrong control instance.
Upon examination, it emerged that the embedded edit controls have the exact same id value.
The relevant java source looks like this:
public class NumberPicker extends LinearLayout implements OnClickListener,
OnFocusChangeListener, OnLongClickListener, OnEditorActionListener {
private final EditText mText;
....
public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
Log.d(TAG, "Numberpicker create, have id: " + getId() );
setOrientation(VERTICAL);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.number_picker, this, true);
...
mText = (EditText) findViewById(R.id.numberpicker_input);
...
}
}
Now, consider the value of R.id.numberpicker_input, when you have three separate instances of a NumberPicker in a layout. In each instance, the edittext field will have the same id.
To get the custom control to work properly, I need the id's of the embedded edit controls to be unique. How can I fix this?
Here's another option, with onSave/RestoreInstanceState(). Doing this the usual way doesn't work, because the framework will restore the EditText value right after the call to onRestoreInstanceState(), overwriting anything you had restored.
To avoid this, clear the id of the embedded control in the constructor of the compound control, eg.:
mText.setId( NO_ID );
Layout elements with id set are ignored while saving/restoring.
Now, save and restore the embedded control value in the compound control state:
protected Parcelable onSaveInstanceState() {
Parcelable p = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putString( "EDIT_TEXT", mText.getText().toString() );
bundle.putParcelable("SUPER", p);
return bundle;
}
protected void onRestoreInstanceState(Parcelable parcel) {
Bundle bundle = (Bundle) parcel;
mText.setText( bundle.getString("EDIT_TEXT") );
super.onRestoreInstanceState(bundle.getParcelable("SUPER"));
}
The restored state is specific to a given instance of the compound object, therefore there will be no interference between multiple instances in the same view.
The advantage of this wrt. previous answer is that no fiddling with id's is necessary.
you may create unique IDs in xml resources as below:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="firstButton"/>
<item type="id" name="secondButton"/>
<item type="id" name="thirdButton"/>
</resources>
and in code, design your EditTexts programmatically and set their ids like:
txt1.setId(R.id.firstButton);
txt2.setId(R.id.secondButton);
txt3.setId(R.id.thirdButton);
for more info, read this
Update.....
another approach would be to iterate through all the views and set them unique ids yourself and keep track of those assigned ids:
first of all, set an id to your parent layout in XML file. It will help us to retrieve the entire layout as ViewGroup. In this example I've set my LinearLayout the id parentLayout
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
ViewGroup parent = (ViewGroup)findViewById(R.id.parentLayout);
for(int i=0; i<parent.getChildCount(); i++){
//at this point i dont know the Id of edittext yet
View v = parent.getChildAt(i);
//if the view is an instance of EditText then lets set to it a new Id
if(v instanceof EditText){
EditText et = (EditText) v;
et.setId(123456);
Toast.makeText(this,"EditText's id: " + et.getId(),
Toast.LENGTH_SHORT).show();
}
}
}
I guess using this technique, you may even assign some random ids to all your views in order to avoid any id collision.
I've been looking at the code for saving instance state in the Android libs. What I noticed is that the control/layout id's are used as keys in the saved state. I think here lies the problem: the id's of the embedded elements in the multiple instances of the compound objects are the same. So on save, the state of just one embedded element is saved, the rest is discarded.
I think the solution lies in making the id's of the embedded elements unique. My solution is this:
Define an attribute which passes an id for the embedded control:
<resources>
<declare-styleable name="compoundcontrol">
<attr name="idEmbedded" format="reference" />
</declare-styleable>
</resources>
Pass this id as parameter to the compound control in the layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:np="http://schemas.android.com/apk/res/org.example.compoundcontrol"
...
/>
...
<org.example.compoundcontrol.CompoundControl
android:id = "#+id/compound_id"
np:idEmbedded="#+id/embedded_id"
/>
...
In the constructor of the compound object, read in this attribute value and use it to set the id of the embedded control.
public CompoundControl(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.compoundcontrol );
int new_id = a.getResourceId( R.styleable.compoundcontrol_idEmbedded, DEFAULT_VALUE );
a.recycle();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.compoundcontrol, this, true);
View mEmbedded = findViewById(R.id.embedded_control);
mEmbedded.setId( new_id );
}
It is unfortunate that the id of the embedded control has to be set so explicitly, but it does the trick.
Another possibility to get new resource ids is this little hack.
public class MyBaseDialogFragment extends DialogFragment implements
ResourceIdGetter {
Random mRandom;
#Override
public int getNewResourceId() {
if(mRandom == null)
mRandom = new Random(123456789); // make them predictable
int i;
while (true) {
i = mRandom.nextInt();
if(i <= 0)
continue;
try {
getResources().getResourceName(i);
} catch (NotFoundException e) {
break;
}
}
return i;
}
}
After that, you can reassign new unique resource id's to controls with ids:
protected mNewResorceId;
public void addEditViewToLinearLayout(LinearLayout pContainer,
LayoutInflater pInflater, ResourceIdGetter pRes) {
View a_multiple_inflated_layout = pInflater.inflate(
R.layout.a_multiple_inflated_layout, pContainer, false);
mNewResorceId = pRes.getNewResourceId()
a_multiple_inflated_layout.findViewById(R.id.child_view_with_id).setId(
mNewResorceId);
pContainer.addView(a_multiple_inflated_layout, new LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
}
After that, you have to use findViewById(mNewResorceId) to get the view.
Because of the given seed for Random, the assigned numbers are always the same. That means, even if the activity is destroyed and recreated, things like cursor position are restored.