extend Android View while using XML? - java

I want to use Android's typical XML to define my layout but I need to override onScrollChanged() in ScrollView. Here is my current attempt which generates a class cast exception:
class MyHorizontalScrollView extends HorizontalScrollView {
private final AbstractChartActivity abstractChartActivity;
public MyHorizontalScrollView(AbstractChartActivity abstractChartActivity,
Context context) {
super(context);
this.abstractChartActivity = abstractChartActivity;
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
String tag = (String) this.getTag();
// if the instance is listening to the column header scroll, then
// move the body
if (tag.equalsIgnoreCase(AbstractChartActivity.COL_HEADER_SCROLL)) {
abstractChartActivity.goodiBodyHorizontalScrollView.scrollTo(l, 0);
} else {
// if the instance is listening to the body scroll, then move
// the header
abstractChartActivity.columnHeaderHorizontalScrollView.scrollTo(l, 0);
}
}
}
and the layout...
<TableLayout
android:id="#+id/blank_cell_above_labels_table"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
</TableLayout>
<HorizontalScrollView
android:id="#+id/column_header_horizontal_scroll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/blank_cell_above_labels_table" >
<TableLayout
android:id="#+id/column_header_table"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</TableLayout>
</HorizontalScrollView>
This line throws a class cast exception because MyHorizontalScrollView extends HorizontalScrollView but is not exactly the same type.
columnHeaderHorizontalScrollView = (HorizontalScrollView) findViewById(R.id.column_header_horizontal_scroll);
I have this all working by programmatically implementing the layout. But I worry it will be a maintenance nightmare so I want to shift functionality to XML where possible.
How can I extend an Android class and use it in XML?
Thanks

Use
<com.example.app.MyHorizontalScrollView
android:id="#+id/column_header_horizontal_scroll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/blank_cell_above_labels_table" >
Where com.example.app is the package name where MyHorizontalScrollView is.
Then
columnHeaderHorizontalScrollView = (MyHorizontalScrollView) findViewById(R.id.column_header_horizontal_scroll);
Also add 2 more constructors
public MyHorizontalScrollView(Context context) {
super(context);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
You may want to read a book my Retro Meir Professional Android Application Development. Chapter 4

Related

Duplicated text in all EditTexts after OnRetainCustomConfigurationInstance

When I use onRetainCustomNonConfigurationInstance to save the text contained inside my custom EditTexts, after I rotate the device the text contained in the last EditText gets duplicated to all the others in the target layout.
While debugging with breakpoints, I found that all the text values are correct all the way. But after the changes are done, all the EditTexts get the same text showing (the text from the last one) and the focus is given to the first one in the layout.
I replicated this behavior in the simplest project I could. I tried with both android API levels 24 and 28.
Where does this behavior come from and how can I fix it ?
MainActivity.java :
public class MainActivity extends AppCompatActivity {
private ArrayList<CustomEdit> editList=new ArrayList<CustomEdit>();
private LinearLayout layout;
private Button addButton;
#RequiresApi(api = Build.VERSION_CODES.M)
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addButton=findViewById(R.id.add_button);
layout = findViewById(R.id.layout);
addButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
addEdit();
}
});
CustomSave data = (CustomSave)getLastCustomNonConfigurationInstance();
if(data==null) return;
for(int i = 0; i<data.texts.size();i++){
addEdit(data.texts.get(i));
}
}
#Override
public Object onRetainCustomNonConfigurationInstance() {
CustomSave data = save();
return data;}
private CustomSave save(){
ArrayList<String> texts = new ArrayList<String>();
for(int i =0; i<editList.size(); i++)
texts.add(editList.get(i).getText());
return new CustomSave(texts);}
/**
* Create a new custom EditText with hint
*/
private void addEdit(){
CustomEdit newEdit = new CustomEdit(this,editList.size());
layout.addView(newEdit,editList.size());
editList.add(newEdit);}
/**
* Create a new custom editText with text
* #param text
*/
private void addEdit(String text){
CustomEdit newEdit;
if(text==null) newEdit = new CustomEdit(this, editList.size());
else newEdit = new CustomEdit(this, editList.size(),text);
layout.addView(newEdit,editList.size());
editList.add(newEdit);}
}
activity_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/title"
android:layout_width="match_parent"
android:layout_height="20dp"
android:text="Title"
android:gravity ="center"/>
<HorizontalScrollView
android:id="#+id/scroll1"
android:layout_below="#id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingLeft="10dp">
<LinearLayout
android:id="#+id/layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="#+id/add_button"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="+"/>
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
CustomEdit.java :
public class CustomEdit extends RelativeLayout {
private EditText editText;
private Button closeButton;
private int indexNumber;
public CustomEdit(Context context, int indexNumber) {
super(context);
this.indexNumber =indexNumber;
init();
}
public CustomEdit(Context context, int indexNumber, String text){
super(context);
this.indexNumber=indexNumber;
init();
editText.setText(text);
}
public CustomEdit(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomEdit(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
init();
}
private void init(){
inflate(getContext(),R.layout.custom_edit_text,this);
editText = (EditText)findViewById(R.id.edit);
editText.setHint("EditText "+(indexNumber+1));
closeButton = (Button)findViewById(R.id.close_button);
closeButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
((MainActivity)getContext()).closeEdit(indexNumber);
}
});
}
public String getText(){
return editText.getText().toString();
}
}
custom_edit_text.xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="40dp">
<EditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/edit"
android:inputType="text"
android:hint="Element"
android:maxLength="30"/>
<Button
android:id="#+id/close_button"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignRight="#id/edit"
android:layout_alignEnd="#id/edit"
android:layout_alignTop="#id/edit"
android:text="X"/>
</RelativeLayout>
CustomSave.java :
public class CustomSave {
public ArrayList<String> texts;
CustomSave(ArrayList<String> texts){
this.texts = texts;
}
}
Thank you.
You have two choices for how to solve this: move your code to a different point in the activity lifecycle or change the xml definition for CustomEdit.
Android lifecycle
Move this code out of onCreate():
CustomSave data = (CustomSave)getLastCustomNonConfigurationInstance();
if(data==null) return;
for(int i = 0; i<data.texts.size();i++){
addEdit(data.texts.get(i));
}
And put it in onRestoreInstanceState() instead:
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
CustomSave data = (CustomSave)getLastCustomNonConfigurationInstance();
if(data==null) return;
for(int i = 0; i<data.texts.size();i++){
addEdit(data.texts.get(i));
}
}
or
XML definition
Add this attribute to the <EditText> tag in your custom_edit_text.xml file:
android:saveEnabled="false"
There's nothing wrong with the code you've written to save/restore your text values. However, after onCreate(), Android is automatically performing its own save/restore logic, and that's overwriting what you've done.
If you move your code from onCreate() to onRestoreInstanceState(), then your code will run after Android's automatic save/restore, so you'll "win". Or, you can disable the automatic save/restore by adding the saveEnabled=false attribute.
The reason that Android's automatic save/restore doesn't work is that it is based on each view's android:id attribute, and your EditText tags all have the same id. That means that all four values get saved with the same key, so the last value overwrites all previous values.
So what's happening here is that Android is also handling state saving for your custom EditText implementation, which is overwriting yours. Since your list of CustomEdit instances is generated dynamically, you likely do not want to rely on Android to save the state for your views, since they do not have unique IDs.
Since your CustomEdit inflates custom_edit_text.xml (which declares the EditText ID as #+id/edit) that means that each CustomEdit you add to the layout has the same ID for the inner EditText -- R.id.edit. Since they all have the same ID, each view will save its state to that ID, so the last one to save its state will end up being the text applied to all of the views when restoring state.
There are two things you can do to avoid this:
In your custom_edit_text.xml, add android:saveEnabled="false" to the EditText. This will prevent that View's state from being saved. This would be preferred as it avoids doing unnecessary work.
Perform your state restoration in onRestoreInstanceState() where the view state is being restored currently.

Set tag abbreviations for custom views on XML

In my app I extend LinearLayout and RelativeLayout, so every time I need to declare a layout in XML (which you may know it's quite often) I need to write a long tag, in order to point to the view's package.
So the XML files look like this:
<com.company.material.widget.LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.company.material.widget.RelativeLayout
android:layout_width="match_parent"
android:layout_height="#dimen/titlebar_height"
android:background="#color/primary"
android:orientation="horizontal">
<TextView
style="#style/Text.Field.Small"
android:id="#+id/form_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<Button
style="#style/Button.Main"
android:id="#+id/form_submit"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
</com.company.material.widget.RelativeLayout>
<com.company.essentials.view.FormView
android:id="#+id/formview"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
This is chaotic!
Is there any way to abbreviate this? Like AppLinearLayout instead of com.company.material.widget.LinearLayout?
Thanks in advance!
You could move your View class to the android.view package. This would allow you to just write <AppRelativeLayout /> instead of <com.company.material.widget.AppRelativeLayout />, since View tag names without package prefixes are 'auto-completed' to the android.view package.
If you don't want to move your whole class to this package, you may just create a dummy sub-class à la:
package android.view;
public class AppRelativeLayout extends com.company.material.widget.RelativeLayout {
// same constructors as super class
public AppRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
You just have to make sure you don't use the same class names as the Android framework already does, such as RelativeLayout, since that would clash with the existing views and layout names. That's why I named the example above AppRelativeLayout.
If it's worth it to you, you can customize your activity's layout inflater.
public class MyActivity extends Activity implements LayoutInflater.Factory {
#Override public void onCreate(Bundle _icicle) {
super.onCreate(_icicle);
setContentView(R.layout.my_activity);
}
#Override public Object getSystemService(String _name) {
Object service = super.getSystemService(_name);
if(LAYOUT_INFLATER_SERVICE.equals(_name)) {
LayoutInflater myInflater = ((LayoutInflater)service).cloneInContext(this);
myInflater.setFactory(this);
return myInflater;
}
return service;
}
#Override public View onCreateView (String _tag, Context _ctx, AttributeSet _as) {
if("mytag".equals(_tag))
return new MyLinearLayout(_ctx, _as);
else
return null;
}
}
setContentView will call getSystemService to obtain a layout inflater, and for each tag that layout inflater will query each of its factories (including our custom one) to see if the factory knows how to create the object that corresponds to that tag.
There is alternative option as well if you like you can do it like this
<view
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.company.material.widget.LinearLayout"
android:orientation="vertical">

Custom linear layout not showing

I am making a custom layout, but it's not showing, and i do not know why.
Here is the XML file where the class is defined
<com.example.name.gw2applicaton.SpecializationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="65dp"
android:layout_height="match_parent">
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
</LinearLayout>
</com.example.name.gw2applicaton.SpecializationView>
Here is the class, just a constructor
public class SpecializationView extends LinearLayout {
public SpecializationView(Context context) {
super(context);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.layout_specialization, this, true);
}
}
And finally where the class is used
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.example.name.gw2applicaton.SpecializationView
android:id="#+id/view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical">
</com.example.name.gw2applicaton.SpecializationView>
</LinearLayout>
</LinearLayout>
The SpecializationView is not visible, I do not know why.
What am I doing wrong here?
That's not how it works for a custom view, as you are trying to do. Use this convention instead:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- just include the layout you defined else where -->
<include layout="#layout/layout_specialization"/>
</LinearLayout>
Where layout_specialization.xml is:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="65dp"
android:layout_height="match_parent">
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
</LinearLayout>
Note: You should use custom view definitions when you need to modify an existing view or viewgroup to have special programatic functionality, such as positioning, dynamic content, niche widget, etc... When you want to use a view like you are where it is just using existing widget functionality, do as I described. The include xml tag is great for defining an xml layout and re-using it through your project so there is a minimized duplication of code.
EDIT:
The reason you layout is not showing by the way is you have only defined the constructor for programmatically creating a view (via java code, not xml). To allow for an xml definition of your custom view extend the class as follows with the additional constructors needd:
public class SpecializationView extends LinearLayout {
/* Programmatic Constructor */
public SpecializationView(Context context) {
super(context);
init(context, null, 0);
}
/* An XML Constructor */
public SpecializationView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
/* An XML Constructor */
public SpecializationView(Context context, AttributeSet attrs, int resId) {
super(context, attrs, resId);
init(context, attrs, resId);
}
/**
* All initialization happens here!
*/
private void init(Context context, AttributeSet attrs, int resId){
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.layout_specialization, this, true);
}
}
This definition now includes the xml ability to create the custom view (which should now probably work for you). The reason it will work is now you send the attribute set, or the attributes definied via xml to the constructor. Since you didn't include it, it doesn't know what to do for your custom view when defined in xml and you cannot access the layout's attributes that you may define as custom.

Best way to achieve grid RecyclerView, where items are square images with some text below it

I'm developing a music store where we show the albums in a grid. Each item is comprised of a cover album (which is a square image) and some text below it which shows artist name, album name, etc. This is the approximate result that I want:
Cover images are not uni-sized, some are 1000x1000, some are 500x500, and maybe there'll be some other sizes.
This is the current xml layout that I use for each item:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/cover"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/artistName"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Here is the important part of the RecyclerView's Adapter, the rest is typical stuff:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.artistName.setText(mAlbums.get(position).name);
Picasso.with(holder.cover.getContext())
.load(mAlbums.get(position).primaryImage)
.into(holder.cover);
}
I get this result:
The right picture is 500x500 and the other two are 1000x1000
However if I add some resize to the images
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.artistName.setText(mAlbums.get(position).name);
Picasso.with(holder.cover.getContext())
.load(mAlbums.get(position).primaryImage)
.resize(300, 300)
.centerInside()
.into(holder.cover);
}
I'll get a better result like:
300 is just a random number. I can replace it with SCREEN_WIDTH/3.
So.... Is there a better approach to tackle this problem (which I think is a very general problem in apps)?
use the following SquareRelativeLayout
package net.simplyadvanced.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
/** A RelativeLayout that will always be square -- same width and height,
* where the height is based off the width. */
public class SquareRelativeLayout extends RelativeLayout {
public SquareRelativeLayout(Context context) {
super(context);
}
public SquareRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(VERSION_CODES.LOLLIPOP)
public SquareRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Set a square layout.
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}
look at the answer
Recyclerview - GridLayoutManager: Set square dimensions
you can do like this. on your activity/fragment.
private void setRecyclerView() {
productRecyclerView.setHasFixedSize(true);
final GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 3);
productRecyclerView.setLayoutManager(layoutManager);
recyclerViewAdapter = new ProductListAdapter(getActivity());
productRecyclerView.setAdapter(recyclerViewAdapter);
}
or you can achieve by adding different viewholders of different row/column heights.
Use a fixed height for item xml Like
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="100dp">
<ImageView
android:id="#+id/cover"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/artistName"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Hope this will solve the problem

Android custom progress bar component

I want to create a progress bar that automatically slides up when incremented. I've sorted the sliding bit (using java), but now want to create a custom progress bar component. This is what I have so far:
public class Smooth_Progress_Bar extends ProgressBar{
public Smooth_Progress_Bar(Context context) {
super(context);
}
}
I'm using this XML code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<com.todddavies.content.smoothprogressbar.Smooth_Progress_Bar
android:layout_width="fill_parent"
style="#android:style/Widget.ProgressBar.Horizontal"
android:layout_height="50dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:progressDrawable="#drawable/pb"/>
</LinearLayout>
I get an immediate force close :/
I strongly suspect I'm doing something majorly wrong with the java bit. I promise to tick the right answer!
I made a project using your code, with the exact same code (even the Object name "Smooth_Progress_Bar").
It all compiled and ran fine.
So this leaves:
You may need all three of the constructors, eg:
public Smooth_Progress_Bar(Context context) {
super(context);
}
public Smooth_Progress_Bar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public Smooth_Progress_Bar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
or your problem is in your progressDrawable. If you drawable is xml, it may contain errors. Try the constructors and if that doesn't work, examine your drawable.

Categories