I am using this ExpandableView component https://github.com/nicolasjafelle/ExpandableView
which has a main class: https://github.com/nicolasjafelle/ExpandableView/blob/master/ExpandableViewProject/expandableview/src/main/java/com/expandable/view/ExpandableView.java
The component wraps other custom components specified by the user. When the custom components have fixed height, everything works fine.
However, I am creating a custom component which has a dynamic number of rows. Suppose this is the initial state:
this is row1
this is row2
this is row3
button
when I click the button, it will show
this is row1
this is row2
this is row3
this is row4
button
When I test my component in a LinearLayout, there is not problem and the new rows are shown. However, when I embed my custom component in ExpandableView, the ContentView of ExpandableView does not extend to wrap the new rows but instead, the button is hidden at the bottom.
I guess that to allow ExpandableView's ContentView to dynamically extend according to the size of the inner custom component I should implement onMeasure() method of ExpandableView. But so far, all my tests have being failed.
The hierarchy of my custom component is:
top
...
ExpandableView (extension of RelativeLayout, no onMeasure method)
LinearLayout (defined in XML)
ExpandableView (extension of RelativeLayout, no onMeasure method)
My custom component (WeekTimes, extension of LinearLayout, no onMeasure method)
Any idea?
I guess this is a bug in ExpandableView component but do not know how to fix it.
Further detail: this is the code for instantiating ExpandableView:
ExpandableView expandableContract = (ExpandableView) formsContained.findViewById(R.id.expandable_academy);
expandableContract.fillData(R.drawable.button_selected_full, getStringResource(R.string.about_academy), false);
expandableContract.addContentView(getLayoutInflater().inflate(R.layout.activity_offer_form_academy, new ExpandableView(this), true));
where R.layout.activity_offer_form_academy is just a layout with my component:
<com.dynassets.assets.util.weektimes.WeekTimes
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/weektimes"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
and the code of my custom component is
public class WeekTimes extends LinearLayout {
public WeekTimes(Context context) {
super(context);
init();
}
public WeekTimes(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
setOrientation(LinearLayout.VERTICAL);
Button button = new Button(getContext());
button.setText("click me");
addView(button);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view) {
try {
Button button = new Button(getContext());
button.setText("click me2");
addView(button);
requestLayout();
} catch (Exception e) {
Log.e("MyApp", "exception", e);
}
}
});
}
}
So, how to tell the parent ExpandableView to extend according to the children's height? Is this a matter of creating a onMeasure() method?
Well, the problem was that ExpandableView contained an animation and this animation was fixing the height preventing the android layouts work as expected. Here is described in further detail:
https://github.com/nicolasjafelle/ExpandableView/issues/5
So the fix is reassigning the height to a relative value after the animation has ended. Issue is fixed by adding these new lines on the ExpandableView extension on the animation method:
animator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
// release height
ViewGroup.LayoutParams layoutParams = contentLayout.getLayoutParams();
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
contentLayout.setLayoutParams(layoutParams);
}
});
I am attempting to create a user interface dynamically. I have successfully create a view and loaded my background image. I have created two additional small view items to display on the background. My problem is that I have not been able to find any advice/instruction that tells me how to draw the small views. It seems that it should be a trivial exercise and I am guessing it is just finding the correct referencing. Hope someone out there can point me in the right direction.
Here is my Activity:
public class GhostActivity extends Activity implements OnTouchListener
{
private DrawView ghostView;
public Card mCard1, mCard2;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
// ToDo add your GUI initialization code here
super.onCreate(savedInstanceState);
// requesting to turn the title OFF
requestWindowFeature(Window.FEATURE_NO_TITLE);
// making it full screen
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
ghostView = new DrawView(this);
setContentView(ghostView);
//get the window size
Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
Context context = getApplicationContext();
//create view items with initial positions
Point startPoint;
startPoint = new Point();
startPoint.x = 5;
startPoint.y = 3;
mCard1 = new Card(context, 1, R.drawable.bol_geel, startPoint);
startPoint.x = 5;
startPoint.y = 43;
mCard2 = new Card(context, 2, R.drawable.bol_rood, startPoint);
//now display them on the ghostView *****************HOW?
// set the callbacks
ghostView.setOnTouchListener(this);
mCard1.setOnTouchListener(this);
mCard2.setOnTouchListener(this);
}
and here is the View;
public class DrawView extends View
{
Drawable bg ;
public DrawView(Context context) {
super(context);
//setFocusable(true);
Drawable bg = this.getResources().getDrawable(R.drawable.bubbleblue480x800);
setBackgroundDrawable(bg);
}
#Override protected void onDraw(Canvas canvas) {
// canvas.drawColor(0x0000000); //if you want another background color
//draw on the canvas
}
}
edit: I believe my problem is needing to pass a pointer to the ghostView canvas. what makes me think that is if I create the children within ghostView then call their .draw method they appear exactly as I would expect.
#Override protected void onDraw(Canvas canvas) {
canvas.drawColor(0x0000000); //if you want another background color
//draw the cards on the canvas
mCard1.draw(canvas);
mCard2.draw(canvas);
}
so at this point I am wondering how to get a reference pointer to the ghostView canvas.
To be honest I am finding the whole Activity - View relationship confusing.
Edit: I have taken a different approach based on detail in this tutorial
http://www.kellbot.com/2009/06/android-hello-circle/
It uses a FrameLayout and it seems I can achieve my objective.
To add view dynamically to view your class must extends from ViewGroup or LinearLayout class then you will able to call method addView.
Inside your ghost view first add a layout e.g Linear or Relative. Then only you could able to add views inside that layout you cant simply add a view to a xml file.
Or you can create a dynamic layout then only u can add view inside that layout.
RelativeLayout relative= new RelativeLayout(findViewById(R.id.your relativeLayoutID));
relative.addView(child);
child could be anything button textview and widget.
I've started working on an app. I build the menu yesterday but the onClick method doesn't work!
I created a class that extends View and called her MainMenuObject - that class is for any object in the main menu (buttons, logos etc). I've created a special class for them because I'm doing an animation when the menu starts. After I've built the MainMenuObject class I've built another class (OpeningTimesView) that extends View and will have all the buttons of the main menu in it, and will function as the main activity's layout.
Everything was good, the animation went very well and I wanted to put listeners on my buttons, so I've added an implemention of onClickListener to the OpeningTimesView class, and overrided the onClick method. Then I've added the listener to the buttons with setOnClickListener(this) and setClickable(true), but it doesn't work! I've tried everything! Please help me figure out what I'm doing wrong. I've added a toast to the onClick method that doesn't depend on any "if" but it's won't show neither.
(BTW is there any way to define the screen width and height as variable that all classes can access? it can't be static because you get the height and width from a display object but there must be another way)
this is the code:
public class OpeningTimesView extends View implements OnClickListener{
private MainMenuObjectView searchButton;
private MainMenuObjectView supportButton;
private MainMenuObjectView aboutButton;
private int screenWidth;
private int screenHeight;
public OpeningTimesView(Context context, Display dis) {
super(context);
this.screenWidth = dis.getWidth();
this.screenHeight = dis.getHeight();
searchButton = new MainMenuObjectView(context, 200, MovingMode.RIGHT, R.drawable.search, dis);
supportButton = new MainMenuObjectView(context, 400, MovingMode.LEFT, R.drawable.support, dis);
aboutButton = new MainMenuObjectView(context, 600, MovingMode.RIGHT, R.drawable.about, dis);
searchButton.setClickable(true);
supportButton.setClickable(true);
aboutButton.setClickable(true);
searchButton.setOnClickListener(this);
supportButton.setOnClickListener(this);
aboutButton.setOnClickListener(this);
}
#Override
public void onClick(View view){
Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
if(view == searchButton){
Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
}
else if(view == supportButton){
Toast.makeText(getContext(), "Support button pressed", Toast.LENGTH_SHORT).show();
}
else Toast.makeText(getContext(), "About button pressed", Toast.LENGTH_SHORT).show();
}
#Override
public void onDraw(Canvas canvas)
{
// Drawing the buttons
this.searchButton.onDraw(canvas);
this.aboutButton.onDraw(canvas);
this.supportButton.onDraw(canvas);
}
Thanks in advance, Elad!
I just had the same Problem - I created a custom view and when I registered a new Listener for it in the activity by calling v.setOnClickListener(new OnClickListener() {...}); the listener just did not get called.
In my custom view I also overwrote the public boolean onTouchEvent(MotionEvent event) {...} method. The problem was that I did not call the method of the View class - super.onTouchEvent(event). That solved the problem. So if you are wondering why your listener does not get called you have probably forgotten to call the superclass'es onTouchEvent method
Here is a simple example:
private static class CustomView extends View implements View.OnClickListener {
public CustomView(Context context) {
super(context);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event); // this super call is important !!!
// YOUR LOGIC HERE
return true;
}
#Override
public void onClick(View v) {
// DO SOMETHING HERE
}
}
Creating custom controls in Android can be tricky if you aren't comfortable with how the UI Framework operates. If you haven't already, I would recommend reading these:
http://developer.android.com/guide/topics/ui/declaring-layout.html
http://developer.android.com/guide/topics/ui/custom-components.html
http://developer.android.com/guide/topics/ui/layout-objects.html
Notice that when layouts are declared in XML the elements are nested. This creates a layout hierarchy that you must create your self when customizing a component using only Java code.
Most likely you are getting caught up in Android's touch hierarchy. Unlike some other popular mobile platforms, Android delivers touch events starting at the top of the View hierarchy and works its way down. The classes that traditionally occupy the higher levels of the hierarchy (Activity and Layouts) have logic in them to forward touches they don't themselves consume.
So, what I would recommend doing is changing your OpeningTimesView to extend a ViewGroup (the superclass of all Android layouts) or a specific layout (LinearLayout, RelativeLayout, etc.) and add your buttons as children. Right now, there does not seem to be a defined hierarchy (the buttons aren't really "contained" in the container, they're just members) which may be confusing the issue as to where events are really going.
The touches should more naturally flow down to the buttons, allowing your click events to trigger
You can take advantage of Android's layout mechanisms to draw your view instead of relying on drawing code to do all of that.
Pick a layout class to start with that will help you place your buttons in their FINAL locations. You can use the animation framework in Android or custom drawing code (like you have now) to animate them anyway you like up to that point. The location of a button and where that button is currently drawn are allowed to be very different if necessary, and that's how the current Animation Framework works in Android (prior to 3.0)...but that's a separate issue. You also have AbsoluteLayout, which allows you to place and replace objects anywhere you like...but be careful of how your app looks on all Android devices with this one (given the different screen sizes).
As to your second point about display info.
The simplest method is probably just to use Context.getResources().getDisplayMetrics() wherever you need it. Activity inherits from Context, so they can call this method directly. Views always have a Context you can access with getContext(). Any other classes you can just pass the Context as a parameter in construction (this is a common pattern in Android, you'll see many objects require a Context, mainly to access Resources).
Here's a skeleton example to jump start things. This just lines the three up horizontally once as a final location:
Public class OpeningTimesView extends LinearLayout implements OnClickListener {
private MainMenuObjectView searchButton;
private MainMenuObjectView supportButton;
private MainMenuObjectView aboutButton;
private int screenWidth;
private int screenHeight;
public OpeningTimesView(Context context) {
this(context, null);
}
//Thus constructor gets used if you ever instantiate your component from XML
public OpeningTimesView(Context context, AttributeSet attrs) {
super(context, attrs);
/* This is a better way to obtain your screen info
DisplayMetrics display = context.getResources().getDisplayMetrics();
screenWidth = display.widthPixels;
screenHeight = display.heightPixels;
*/
//This way works also, without needing to customize the constructor
WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Display dis = wm.getDefaultDisplay();
screenWidth = dis.getWidth();
screenHeight = dis.getHeight();
searchButton = new MainMenuObjectView(context, 200, MovingMode.RIGHT, R.drawable.search, dis);
supportButton = new MainMenuObjectView(context, 400, MovingMode.LEFT, R.drawable.support, dis);
aboutButton = new MainMenuObjectView(context, 600, MovingMode.RIGHT, R.drawable.about, dis);
//Even if they don't extend button, if MainMenuObjectView is always clickable
// this should probably be brought into that class's constructor
searchButton.setClickable(true);
supportButton.setClickable(true);
aboutButton.setClickable(true);
searchButton.setOnClickListener(this);
supportButton.setOnClickListener(this);
aboutButton.setOnClickListener(this);
//Add the buttons to the layout (the buttons are now children of the container)
setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addView(searchButton, params);
addView(supportButton, params);
addView(aboutButton, params);
}
#Override
public void onClick(View view){
Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
if(view == searchButton){
Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
}
else if(view == supportButton){
Toast.makeText(getContext(), "Support button pressed", Toast.LENGTH_SHORT).show();
}
else Toast.makeText(getContext(), "About button pressed", Toast.LENGTH_SHORT).show();
}
#Override
public void onDraw(Canvas canvas)
{
//Drawing the buttons
// This may only be necessary until they are in place, then just call super.onDraw(canvas)
this.searchButton.onDraw(canvas);
this.aboutButton.onDraw(canvas);
this.supportButton.onDraw(canvas);
}
}
You can customize this from there. Perhaps starting the buttons with visibility set to View.INVISIBLE until you animate them in with your drawing code or a custom Animation object, then making them visibile in their final resting place.
The key here, though, is the the layout is smart enough to know that when it receives a touch event it is supposed to forward it to the corresponding child. You can create a custom view without this, but you will have to intercept all touches on the container and do the math to determine which subview to manually forward the event to. If you truly can't make any layout manager work, this is your recourse.
Hope that Helps!
You can just call performClick() in onTouchEvent of your custom view.
Use this in you custom view:
#Override
public boolean onTouchEvent(final MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_UP){
return performClick();
}
return true;
}
I do this so:
public class YourView extends LinearLayout implements OnClickListener {
OnClickListener listener;
//... constructors
public void setOnClickListener(OnClickListener listener) {
this.listener = listener;
}
#Override
public void onClick(View v) {
if (listener != null)
listener.onClick(v);
}
}
You have to call setOnClickListener(this) in contructor(s) and implement View.OnClickListener on self.
In this way:
public class MyView extends View implements View.OnClickListener {
public MyView(Context context) {
super(context);
setOnClickListener(this);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnClickListener(this);
}
#Override
public void onClick(View v) {
Toast.makeText(getContext(), "On click.", Toast.LENGTH_SHORT).show();
}
}
I had the same problem. In my case I had a LinearLayout as a root element of my custom view, with clickable and focusable set to true, and the custom view tag itself (used in a fragment's layout) was also set to be clickable and focusable. Turns out that the only thing I had to do to get it working was to remove all the clickable and focusable attributes from within the XML :) Counter-intuitive, but it worked.
Implement the onClickListener in the MainMenuObjectView class, since those are the objects that will respond to clicks.
Another alternative would be to extend Button instead of View, because you are using only buttons in there
Update: Full example
This is the idea to implement it directly into the clickable views. There is a TestView class that extends View and overrides onDraw, as you need it to, and also responds to clicks. I left out any animation implementation as you have that part and it's not relevant to the ClickListener discussion.
I tested it in an Eclair emulator and it works as expected (a Toast message after a click).
file: Test.java
package com.aleadam.test;
import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.TextView;
public class Test extends Activity {
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout ll = new LinearLayout(this);
ll.setOrientation(LinearLayout.VERTICAL);
TextView label = new TextView(this);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
label.setText("Click the circle!");
TestView testView = new TestView(this);
ll.addView(label, layoutParams);
ll.addView(testView, layoutParams);
setContentView(ll);
}
}
file: TestView.java
package com.aleadam.test;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;
public class TestView extends View implements OnClickListener {
Context context;
public TestView(Context context) {
super(context);
this.context = context;
setOnClickListener(this);
}
public void onClick(View arg0) {
Toast.makeText(context, "View clicked.", Toast.LENGTH_SHORT).show();
}
#Override
public void onDraw (Canvas canvas) {
super.onDraw(canvas);
this.setBackgroundColor(Color.LTGRAY);
Paint paint = new Paint (Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
canvas.drawCircle(20, 20, 20, paint);
}
}
If you need some clickable and some not clickable, you can add a constructor with a
boolean argument to determine whether the ClickListener is attached or not to the View:
public TestView(Context context, boolean clickable) {
super(context);
this.context = context;
if (clickable)
setOnClickListener(this);
}
I've got a solution!
It's not really a solution for this specific issue, but a whole new approach.
I sent this thread to somebody I know and he told me to use the Animation SDK the android has (like Wireless Designs mentioned), so instead of doing the main menu page with 4 classes, I'm doing it only with one class that extends Activity, and the Animation class offers many animation options.
I want to thank you both for helping me, you are great.
I'm adding the code if someone will encounter this thread with the same problem or something:
package elad.openapp;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.Toast;
public class OpeningTimes extends Activity implements OnClickListener{
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Disabling the title bar..
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
// Create the buttons and title objects
ImageView title = (ImageView)findViewById(R.id.title_main);
ImageView search = (ImageView)findViewById(R.id.search_button_main);
ImageView support = (ImageView)findViewById(R.id.support_button_main);
ImageView about = (ImageView)findViewById(R.id.about_button_main);
// Setting the onClick listeners
search.setOnClickListener(this);
support.setOnClickListener(this);
about.setOnClickListener(this);
setButtonsAnimation(title, search, support, about);
}
#Override
public void onClick(View v) {
if(v.getId()==R.id.search_button_main){
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
startActivity(new Intent(this,SearchPage.class));
}
else if(v.getId()==R.id.support_button_main){
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
Toast.makeText(this, "Coming soon...", Toast.LENGTH_LONG).show();
}
else if(v.getId()==R.id.about_button_main){
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
Toast.makeText(this, "Coming soon...", Toast.LENGTH_LONG).show();
}
}
// Setting the animation on the buttons
public void setButtonsAnimation(ImageView title, ImageView search, ImageView support, ImageView about){
// Title animation (two animations - scale and translate)
AnimationSet animSet = new AnimationSet(true);
Animation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f);
anim.setDuration(750);
animSet.addAnimation(anim);
anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, -1.0f, Animation.RELATIVE_TO_SELF, 0.0f);
anim.setDuration(750);
animSet.addAnimation(anim);
title.startAnimation(animSet);
// Search button animation
anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -1.5f, Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f);
anim.setDuration(750);
search.startAnimation(anim);
// Support button animation
anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 1.5f, Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f);
anim.setDuration(750);
support.startAnimation(anim);
// About button animation
anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, 3f, Animation.RELATIVE_TO_SELF, 0.0f);
anim.setDuration(750);
about.startAnimation(anim);
}
}
In my case I had a RelativeLayout as a parent in my custom view and the only way to make it work was to set focusable and clickable to true in the RelativeLayout and in the constructor of the custom view, after inflating the layout, add this:
View view = inflate(getContext(), R.layout.layout_my_custom_view, this);
view.findViewById(R.id.theparent).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view) {
performClick();
}
});
It is this easy:
public class FancyButton
extends FrameLayout
implements View.OnClickListener { ..
void yourSetupFunction(Context context, #Nullable AttributeSet attrs) {
..
super.setOnClickListener(this); // NOTE THE SUPER
}
OnClickListener consumerListener = null;
#Override
public void setOnClickListener(#Nullable OnClickListener l) {
consumerListener = l;
// DO NOT CALL SUPER HERE
}
#Override
public void onClick(View v) {
Log.i("dev","perform my custom functions, and then ...");
if (consumerListener != null) { consumerListener.onClick(v); }
}
implement View.OnClickListener, and hence have an
onClick(View v)
in setOnClickListener, "remember" the listener set from the outside world
set the actual listener to be us (using "super." ...)
in onClick do you custom stuff, and then call the outside world's onClick
just add callOnClick() to your onTouchEvent() method
public boolean onTouchEvent(MotionEvent event) {
.....YOURCODE.....
callOnClick();
return boolean;
}