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.
Related
I'm trying to create a UI similar to Google Keep. I know how to create a staggered View using a Recycler View. If i click a specific Card. Then it has to open a activity.
I can achieve this using onclick method.
This same scenario happens in atleast 5 different Activities in my App.
My question is that
Can I use this single Adapter in all those 5 places ?
If yes, where should i place the onclick actions ?
If no, How can I Create a staggered layout like Keep?
Thanks in Advance!
(See application for RecyclerView below in edits)
Like I mentioned in my comment, it's certainly fine to have separate adapters for all your Activities which use different data and views. As your app data and layouts get more complex, so does your code...that's just the way it is.
But if some of your Activities used similar data in their ListViews -- maybe, for example, two TextViews and an ImageButton -- you could save some effort by defining a single adapter that can be used for multiple Activities. You would then instantiate separate objects for each Activity, similar to the way you would create several ArrayAdapter<String> objects to populate multiple ListViews.
The BaseAdapter is a great class to extend when writing a custom adapter. It's flexible and allows you complete control over the data that's getting shown in your ListView. Here's a minimal example:
public class CustomBaseAdapter extends BaseAdapter {
private Context context;
private ArrayList<String> listData;
public CustomBaseAdapter(Context context, ArrayList<String> listData) {
this.context = context;
this.listData = listData;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.your_list_item_layout, parent, false);
//populate the view with your data -- some examples...
TextView textData = (TextView) convertView.findViewById(R.id.yourTextView);
textData.setText(listData.get(position));
ImageButton button = (ImageButton) convertView.findViewById(R.id.yourImageButton);
button.setOnClickListener(new View.OnClickListener() {
//...
//...
});
}
return convertView;
}
#Override
public Object getItem(int position) {
return 0;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public int getCount() {
return listData.size();
}
}
So the key part of this code is obviously the getView() method, which is called every time the ListView needs some data to display. For efficiency, views are stored in something called a convertView so they may be re-used and not have to be inflated every time a view appears on the screen.
So what we do in getView() is first find out if the convertView exists. If it does, we just pass that back to the calling ListView because everything should already be instantiated and ready to go. If the convertView is null, it means the data hasn't been instantiated (or needs to be re-instantiated for whatever reason), and so we inflate a brand new view and populate it with our data.
So in the case of this example adapter above, if several of your Activities all displayed a single list of Strings, you could reuse this adapter for each one, passing in a different ArrayList<String> through the constructor each time you created a new object. But obviously you could pass in more than just Strings if you had more data to show. The level of complexity is up to you. And if the difference among your Activities was too great, I would just create custom versions of this class for all of them and just instantiate them and populate them however you'd like. It will keep all your data very organized and encapsulated.
Hope this helps! Feel free to ask questions for more clarification if you need it.
EDIT IN RESPONSE TO COMMENTS
Since you are using a RecyclerView instead of just plain ListViews (which I, for some reason, totally forgot) you could still do something very similar using a RecyclerView.Adapter<YourViewHolder> instead. The difference would be that instead of inflating the views in a getView() method, they are inflated inside your custom ViewHolder, which I assume you already have. The code might look something like this:
public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<StringViewHolder> {
private final List<String> items;
public CustomRecyclerViewAdapter(ArrayList<String> items) {
this.items = items;
}
#Override
public StringViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//So instead of inflating the views here or in a getView() like in
//in the BaseAdapter, you would instead inflate them in your custom
//ViewHolder.
return new StringViewHolder(parent);
}
#Override
public void onBindViewHolder(StringViewHolder holder, int position) {
holder.setModel(items.get(position));
}
#Override
public long getItemId(int position) {
return items.get(position).hashCode();
}
#Override
public int getItemCount() {
return items.size();
}
}
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.
I have an ArrayAdapter class that creates comment boxes. There is a button within the comment box that will be either blue or black. The color of the button is dependent on an array which is received through JSON. If the array looks like this "NO","NO","YES","NO","NO","NO" the third button will have blue text. My JSON and ArrayAdapter class create 7 comment boxes at a time. The problem is once the code changes a button to blue it continuously changes the button blue. By this I mean if an array is received that looks like this "NO","NO","YES","NO","NO","NO" the third button will be blue, then I receive another set of comments so this time the array looks like this "NO","NO","NO","NO","NO","NO" according to this code no button should be blue, but for some reason the third button is still blue. I could load multiple more sets of comments and the third button will always be blue even though the code clearly says it should be black. Strangely the button will be blue but will act as if it were a black button. Here is my ArrayAdapter,
class ListAdapter extends ArrayAdapter<Item> {
public ListAdapter(Context context, int textViewResourceId) {
super(context, textViewResourceId);
}
private List<Item> items;
public ListAdapter(Context context, int resource, List<Item> items) {
super(context, resource, items);
this.items = items;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.list_item_layout, null);
}
final Item p = items.get(position);
if (p != null) {
//set xml objects
//must be done inside of class
ButtonListViewItem = (TextView) v.findViewById(R.id.button_listview_item);
if(p.getJSONArray().equals("NO")){
ButtonListViewItem.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
ButtonListViewItem.setTextColor(0xff000000);
new AsyncTask().execute();
}//end on click
});
}//end if equals NO
if(p.getJSONArray().equals("YES")){
ButtonListViewItem.setClickable(false);
ButtonListViewItem.setTextColor(0xff3399FF);
}//end if equals yes
}//end if null
return v;
}//end getView
}//end ListAdapter class
The text color is wrong because you're not correctly handling recycled views.
The shortest and simplest solution is to remove this check:
if (v == null)
and inflate a new view every time. This is less efficient, but will make your code easier to work with.
The solution if you opt to continue using recycled views is to explicitly set the text color and clickability of the button every time:
if (p.getJSONArray().equals("YES")) {
ButtonListViewItem.setClickable(false);
ButtonListViewItem.setTextColor(0xff3399FF);
} else {
ButtonListViewItem.setClickable(true);
ButtonListViewItem.setTextColor(0xff000000);
}
The reason you need to do that is because recycled views are handed over just as you left them, changed attributes and all. They will no longer match your XML layout. So when a view that was previously tied to a "YES" is recycled, the changes you made to the text color will still be in place: the text will be blue and the button won't be clickable.
Inflating a new view allows you start in a known state every time---you'll always have something that starts off matching your XML. The tradeoff is efficiency, inflating views is relatively expensive. If your apps needs to be more efficient you should look into the view holder pattern as well, as finding views is also an expense that can be avoided.
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 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.