I need to modify custom View's ViewGroup.MarginLayoutParams depending on some inner state of the View.
The task looks like this:
Set some of the ViewGroup.MarginLayoutParams during View's initialization at the same time saving them to keep track of what was set via View's layout attributes (layout_marginTop, etc).
At the desired moments in the future, change ViewGroup.MarginLayoutParams using either stored values or some other ones.
Hence, I have 2 questions:
What is the right place to save ViewGroup.MarginLayoutParams set via the layout attributes?
Can't do it in the View's constructor, since the parent ViewGroup hasn't yet assigned it. Is onAttachedToWindow() the correct place for that?
Is onMeasure() the correct place for adjusting ViewGroup.MarginLayoutParams as needed?
Something like this:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();
params.topMargin = ...;
setLayoutParams(params);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Since in the code above we're calling setLayoutParams() in onMeasure() (when the current layout pass in progress), would it cause the second layout pass?
You can just keep a reference to the ViewGroup that you're working with, and call getLayoutParams as needed. You'll need to make the params variable a more specific instance of ViewGroup- You could instantiate it as
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams();
Then, you can just call your choice of
params.setMarginStart(int);
params.setMarginEnd(int);
params.setMargins(int, int, int, int);
to set the margins. Note that you do NOT need to call setLayoutParams() at all to have the changes apply. The params variable and view.getLayoutParams() reference the same object, so any changes you make to one will apply to the other.
A full code example could look like the following:
public final class MyActivity extends AppCompatActivity {
RelativeLayout parent;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
parent = (RelativeLayout) findViewById(R.id.parent);
}
private void setParentMargin(int left, int top, int right, int bottom) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) parent.getLayoutParams();
params.setMargins(left, top, right, bottom);
params.setMarginStart(left);
params.setMarginEnd(right);
}
}
As long as you call that method after onCreate, you should be fine.
The answer to your second question depends on what event is triggering the change in margin. If it's whenever the activity gets recreated, you could do it in onResume (because that's guaranteed to happen after onCreate). You could also call it from an onClickListener, or from an interface callback. So, you do not necessarily have to set the margins in onMeasure.
Related
Every time I run my app it crashes giving me a nullpointerexception, I want to programatically change my background depending on the scenario, here is my code:
Main Activity:
public class Activity extends AppCompatActivity {
ConstraintLayout layout;
String messageSafe = "Item is Safe for Consumption";
String messageUnSafe = "Item is NOT Safe for Consumption";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_information);
layout = new ConstraintLayout(this);
if (matched.length == 0) {
layout.setBackgroundResource(R.drawable.background_safe);
setContentView(layout);
changeColor("#00FF00");
messageView.setText(messageSafe);
}
else{
layout.setBackgroundResource(R.drawable.background_unsafe);
setContentView(layout);
changeColor("#FF0000");
messageView.setText(messageUnSafe);
}
ListView listContains = (ListView) findViewById(R.id.lvItemsFound);
ArrayAdapter<String> contains = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, foundItems);
listContains.setAdapter(contains);
ListView listRestricted = (ListView) findViewById(R.id.lvItemsRestricted);
ArrayAdapter<String> found = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, matched);
listRestricted.setAdapter(found);
}
You are losing reference to your old view because you changed the layout to a new ConstraintLayout object. This means you now don't have your ListView objects and other items in your XML because that View is gone. It's not the ContentView anymore. If you want to work on the existing layout, you need to give the root view an ID.
<constraintlayout android:id="#+id/container" ... />
Then you can reference that ID with findViewById(R.id.container) and use the object you get from it to change your background like you are doing.
Try this:
1. Give your root view an ID
2. Set a ConstraintLayout object with ConstraintLayout layout = findViewById(R.id.container) (Note: You can call it anything, not just container, I am just going off my example from above, since I gave it the ID 'container')
3. call setBackgroundResource() like you are doing.
4. No need to call setContentView() again, this was set in the beginning, and you do not want to reset it to a new view you just constructed like you were initially doing.
5. You shouldn't crash when trying to call setAdapter() to your ListView now because you don't have a reference to an object that isn't in your content view.
layout = (ConstraintLayout)findViewById(R.id.container);
if (matched.length == 0) {
layout.setBackgroundResource(R.drawable.background_safe);
changeColor("#00FF00"); //assuming this is some local function?
messageView.setText(messageSafe);
}
else{
layout.setBackgroundResource(R.drawable.background_unsafe);
changeColor("#FF0000");
messageView.setText(messageUnSafe);
}
You are trying to set the background by replacing the view of your activity (this is what setContentView() does). This causes a null pointer exception later because the old layout (defined in the XML) has been replaced, so your list view no longer exists.
Instead, you should get a reference to the existing root view (the ConstraintLayout, although if you're just setting background you can just reference it as a View, no need to be so specific), and set the background on it, like so:
findViewById(R.id.container).setBackgroundResource(R.drawable.unsafe);
You'll also need to give the containing layout an id in the existing layout XML:
<android.support.constraint.ConstraintLayout
android:id="#+id/container"
... etc.
I'm trying to do something like this in android:
// If the parent is a viewpager
if (parentIsViewPager)
{
// Retrieve the view's layout informations specific to viewpagers
ViewPager.MarginLayoutParams marginLayoutParams = (ViewPager.MarginLayoutParams) getLayoutParams();
}
// If the parent is not a viewpager (mainly a viewgroup)
else
{
// Retrieve the view's layout informations for a viewgroup
ViewGroup.MarginLayoutParams marginLayoutParams = getLayoutParams();
}
Log.i("Test","marginLayoutParams.leftMargin = " + marginLayoutParams.leftMargin);
Unfortunately, the IDE (Android Studio 1.5.1) tells me it cannot find declaration for layoutParams in the last line above... But I declared it in the if statements!
I guess there is something about scope in here but as the following code in my project is really big, I cannot duplicate it in each if statement.
So how can I achieve this or something similar?
EDIT:
As I guessed, and this was confirmed in the comments, this is a matter of scope. Ok, got it.
Thanks for your help guys.
A variable declaration is limited to it's scope.
In this case the if-else statement.
You could do it like this:
// Declare in correct scope and define later...
ViewGroup.LayoutParams layoutParams;
// If the parent is a viewpager
if (parentIsViewPager)
{
// Retrieve the view's layout informations specific to viewpagers
layoutParams = (ViewGroup.LayoutParams) getLayoutParams();
}
// If the parent is not a viewpager (mainly a viewgroup)
else
{
// Retrieve the view's layout informations for a viewgroup
layoutParams = (ViewGroup.LayoutParams) getLayoutParams();
}
Log.i("Test","layoutParams.width = " + layoutParams.width);
This compilation error has nothing to do with android or the IDE, is just the following:
if (parentIsViewPager)
{
// Retrieve the view's layout informations specific to viewpagers
ViewPager.LayoutParams layoutParams = (ViewPager.LayoutParams) getLayoutParams();
}
in this condition, the "Variable" layoutParams has a Scope: only inside the if condition,
the second part:
else
{
// Retrieve the view's layout informations for a viewgroup
ViewGroup.LayoutParams layoutParams = getLayoutParams();
}
the same criteria applies..
the error is because you are trying to use an object out of its scope...
layoutParams is not avaliable anymore outside that if condition
Is simply duplicating the Log statement on both sides of the if-else not an option or is layoutParams referenced else where?
// If the parent is a viewpager
if (parentIsViewPager)
{
// Retrieve the view's layout informations specific to viewpagers
ViewPager.LayoutParams layoutParams = (ViewPager.LayoutParams) getLayoutParams();
Log.i("Test","layoutParams.width = " + layoutParams.width);
}
// If the parent is not a viewpager (mainly a viewgroup)
else
{
// Retrieve the view's layout informations for a viewgroup
ViewGroup.LayoutParams layoutParams = getLayoutParams();
Log.i("Test","layoutParams.width = " + layoutParams.width);
}
First of all i'm not even sure if this is possible.
I'm trying the achieve the following: Create my own seperate dependency which dynamically adds onclick listeners to certain elements for which the id's are known. (Entirely outside an activity; see the following:)
public class Example extends Application
What i managed so far is accessing specific view elements from within my own class. For this i am using the LayoutInflator:
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.activity_main, null);
View element = view.findViewById(R.id.readthis);
After retrieving the specific view element i add an onclickListener (in this example a button)
button.setOnClickListener(getOnClickDoSomething(button));
View.OnClickListener getOnClickDoSomething(final Button button) {
return new View.OnClickListener() {
public void onClick(View v) {
button.setText("text now set.. ");
}
};
}
Setting the thing works fine. But i'm guessing the layout inflator actually creates a copy of the view and i'm not getting direct references to the currently active view objects. Which means the onclickListener isn't being triggered because it's not set to the proper object.
I'm fairly new to Android and hence the question whether i'm wasting my time or if there's a way to get this to work. Would love to get some advice on this.
I'm not sure if I got your question right, but you can expose setter methods in the activity that contains the relevant views, to set a click listener from outside. Something like this:
public void setViewClickListener(OnClickListener clickListener) {
if (myView != null) {
myView.setOnClickListener(clickListener);
}
}
And then in your separate class, call this method with the relevant click listener.
Resolved! I was looking at this the wrong way. Solved it by implementing Application.ActivityLifecycleCallbacks and registering my class by registerActivityLifecycleCallbacks(this)
Now i can access the views from the activity context within the callbacks.
I want to keep my application thin.
Problem: I would like to reuse my Fragment class code to create 3 different instances in the ViewPager which will have 3 pages. Each Fragment will have a different ImageView or background Drawable. What are best practices regarding this? I noticed that using factory methods like here seem to be good, any other alternatives?
I have one Fragment which has the following methods:
Fragment.java
public static Fragment newInstance(Context context) {
FragmentTutorial f = new FragmentTutorial();
Bundle args = new Bundle();
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment, null);
return root;
}
I have a ViewPagerAdapter class which has the following methods:
ViewPagerAdapter.java
public ViewPagerAdapter(Context context, FragmentManager fm) {
super(fm);
mContext = context;
}
#Override
public Fragment getItem(int position) {
return new FragmentTutorial().newInstance(mContext);
}
#Override
public int getCount() {
return totalPage;
}
What I've found is the "best" way to do it (in my opinion, of course) is to do the following:
Have the fragment contain methods to set the customizable data (background, text, etc)
Note: Be careful of trying to load the data in when first creating the fragment. You may be able to set the data before onCreateView() even runs, or at other times it may run after onCreateView(). I personally use a boolean to check if the data has been set. Inside onCreateView() [or onActivityCreated()], I check if the data has been set already. If it has, load in the data. Alternatively, while setting the data, I check if the views have been created/cached already. This is done by simply having variables to cache the data, say private ImageView mBackgroundView. If the view is not null, then I safely set the data on the views.
The above is also an alternative to using newInstance, although both methods work pretty well. However, for more flexibility, I only use newInstance if a) the data is already known before the fragment has to be inserted and b) the data doesn't need to change according to input from elsewhere much.
Let the ViewPager handle all the data
Pass in all the data - a list of ImageViews, a array of Strings, define where all the data is in Resources, etc - at the very beginning [say, in the constructor]
Have the ViewPager create an ArrayList of the fragments- set up each fragment as early as possible (say when first getting all the data) and add it to the list
Let getCount() just use the size of the list
Let getItem() just get the item in the list at the position
Note: If you have any dynamic data, set it up in the getItem() method. Furthermore, you can always add more data+fragments during runtime as well [just notify the adapter that the dataset has been changed]
Essentially, the fragment is like a simple servant- it does simply the least work necessary. If it doesn't have to handle choosing the data, all the better. It'll thus be far more flexible. Just give methods to set the data/views appropriately on the fragment. Now, the ArrayAdapter can do all the grimy hard work with managing the data and giving it to the appropriate fragment. Take advantage of that.
Now, note that this is assuming you want to use a single layout but want to change different aspects of that layout (texts, background, etc). If you want to make a master fragment class that can use any sort of defined layout, you can but note that it decreases the runtime flexibility (how can you change the text or background to something you get from the internet? You simply can't if you only can define and choose from pre-set layouts).
Either way, the ArrayAdapter should take care of all the different data while the fragment simply does as it's designed to do, in a more flexible manner preferably.
Edit:
Here is the project where I most recently implemented this sort of pattern. Note that it has far more to it, so I'll replace it with some not-so-pseudo pseudo-code in the morning/afternoon.
ViewPager [a bit sloppy with all the different things I was trying to do, including extending from a FragmentStatePagerAdapter without actually using any of the specific features of a StatePagerAdapter. In other words, I still need to work on the lifecycle implementations everywhere]
Fragment [Also may be a bit sloppy but shows the pattern still]
The object (actually another fragment) that uses the ViewPager [it's actually a "VerticalViewpager" from a library, but other than the animations and direction to change the current fragment, it's exactly the same- particularly code-wise]
Edit2:
Here is a more (if overly) simplified example of the pattern described above.
Disclaimer: The following code has absolutely no lifecycle management implementations and is older code that has been untouched since around August '14
Fragment simply allows the user of the fragment to set the background color and the text of the single TextView
Link to BaseFragment
Link to layout file
The adapter creates three instances of the fragment and sets the background color and text of each. Each fragment's text, color, and total fragments is hard coded.
Link to Activity+adapter
Link to layout file
Now, here are the exact relevant portions of the code:
BaseFragment
// Note: Found out later can extend normal Fragments but must use v13 adapter
public class BaseFragment extends android.support.v4.app.Fragment {
FrameLayout mMainLayout; // The parent layout
int mNewColor = 0; // The new bg color, set from activity
String mNewText = ""; // The new text, set from activity
TextView mMainText; // The only textview in this fragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the fragment's layout
View view = inflater.inflate(R.layout.fragment_base,container,false);
// Save the textview for further editing
mMainText = (TextView) view.findViewById(R.id.textView);
// Save the framelayout to change background color later
mMainLayout = (FrameLayout) view.findViewById(R.id.mainLayout);
return view;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// If there is new text or color assigned, set em
if(mNewText != ""){
mMainText.setText(mNewText);
}
if(mNewColor != 0){
mMainLayout.setBackgroundColor(mNewColor);
}
}
#Override
public void onStart(){
super.onStart();
}
// Simply indicate to change the text of the fragment
public void changeText(String newText){
mNewText=newText;
}
// Simply indicate to change the background color of the fragment
public void changeBG(int color) {
// If no color was passed, then set background to white
if(color == 0)
{
mNewColor=getResources().getColor(R.color.white);
}
// else set the color to what was passed in
else{
mNewColor=color;
}
}
}
MyAdapter
class MyAdapter extends FragmentPagerAdapter{
// Three simple fragments
BaseFragment fragA;
BaseFragment fragB;
BaseFragment fragC;
public MyAdapter(FragmentManager fm) {
super(fm);
}
public void setFragments(Context c){
// Set up the simple base fragments
fragA = new BaseFragment();
fragB = new BaseFragment();
fragC = new BaseFragment();
Resources res = c.getResources();
fragA.changeText("This is Fragment A!");
fragB.changeText("This is Fragment B!");
fragC.changeText("This is Fragment C!");
fragA.changeBG(res.getColor(R.color.dev_blue));
fragB.changeBG(res.getColor(R.color.dev_green));
fragC.changeBG(res.getColor(R.color.dev_orange));
}
#Override
public Fragment getItem(int position) {
// TODO: Make this more efficient, use a list or such, also comment more
Fragment frag = null;
if(position == 0){
frag = fragA;
}
else if(position == 1){
frag = fragB;
}
else if(position == 2){
frag = fragC;
}
return frag;
}
#Override
public int getCount() {
return 3;
}
}
You need to pass some sort of id along with newInstance() while creating instance. And according to that id you can use if..else to choose layout file.
See my reference code below:
int id;
public static Fragment newInstance(Context context, int id) {
FragmentTutorial f = new FragmentTutorial();
Bundle args = new Bundle();
this.id = id;
return f;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(id == 1)
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment1, null);
else
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment2, null);
return root;
}
Can't you just introduce fields to the Fragment class to account for the variances in background, etc. and add them to its constructor? Then in getItem instantiate the Fragment class with different values depending on the value of position.
i currently create an app that needs an custom listview. Everything with my listview is fine, but now i neet to know, how to set an onClickListener to an view, defined in my list_row.xml. i just want the onclicklistener on the whole item, and on this one inner view. I attach a picture to demonstrate my problem, because it is so hard to describe >.<
Picture (dropbox): https://www.dropbox.com/s/72xdxuwz47vl7s5/problem.png
I need a function that is called when clicking into the view [my Problem] indicates. its an ImageView filled with an image.
Here's something I've done before that seems pretty similar to what you want to accomplish.
First, you declare an onItemClickListener for your ListView. This will handle standard list item taps (that is, taps inside a list item but outside the inner view region that you're concerned about). You can do this in a variety of places in your code, but onCreate() is a common one.
Example:
mListView.setOnItemClickListener( new OnItemClickListener() {
#Override
public void onItemClick( AdapterView<?> parent, View view, int position, long id ) {
// Handle standard list item tap
// ...
}
} );
Then, you can just declare whatever onClickListeners you need for your inner view(s) inside your adapter's getView() method to handle click/tap events on your inner view.
Example:
#Override
public View getView( int position, View convertView, ViewGroup parent ) {
LinearLayout itemView;
// Inflate layout XML, etc.
// ...
// Find subviews in layout
ImageView innerView = (ImageView) itemView.findViewById( R.id.myInnerViewId );
// ...
// Set up onClickListener for inner view
innerView.setOnClickListener( new OnClickListener() {
#Override
public void onClick( View v ) {
// Handle inner view tap
// ...
}
} );
// ...
}
To set an OnClickListener in each row simply extend your current Adapter and override the getView() method. In there you can define specific listeners as you normally would.
This is discussed in great detail in this Google Talk by Romain Guy.