How to implement scrolling in Android custom view - android

I've written a custom view which I'd like to ensure is viewable on any screen size.
I've overridden the onMeasure:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth, parentHeight);
}
and this seems to work fine when the view is smaller than the screen. Sometimes, though, the custom view is larger than the screen, and I'm planning for smaller screen sizes, so I wrapped the custom view in a ScrollView but now the parentHeight in the onMeasure comes out as 0.
I've changed the superclass of the custom view from View to ScrollView, hoping for an easy win of inheriting the scrolling functionality, but this hasn't happened so I'm left with trying to find a way of getting the ScrollView functionality to work with my custom view, or writing my own scrolling functionality.
Has anyone any advice? I've seen this post on Large Image Scrolling Using Low Level Touch Events and was going to copy some of that functionality if I'm forced to write my own, but would appreciate a nudge in the right direction either way.

Turns out the answer was simple. I left the ScrollView in and changed my onMeasure. The thing to note about it is that although Android would supply the width, it wouldn't supply me with a height, which was initially confusing. To just get the view to fill the available space I grabbed the visible rect of the parent view. Code in full (hopefully it'll help someone else who is having the same problem):
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight;
if(isLandscape()) {
Rect r = new Rect();
((ScrollView)getParent()).getGlobalVisibleRect(r);
parentHeight = r.bottom - r.top;
} else {
parentHeight = (int) Util.getViewHeight();
}
this.setMeasuredDimension(parentWidth, parentHeight);
}

Related

How to square a custom view when layout_height and layout_width are set to have different magnitudes?

As per my understanding when user specifies a views height/width in pixels/dp, the view receives EXACTLY spec mode. EXACTLY spec mode means that the view must use this size. Suppose I want to make sure my view remains a square. How can that be achieved if the user sets different values for layout_height and layout_width.
Override onMeasure for your custom view, determine what the smallest dimension is, and set both your width and height to that minimum dimension.
Something like:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
int lesserDimension = (specWidth > specHeight)?specHeight:specWidth;
setMeasuredDimension(lesserDimension, lesserDimension);
}

If inflater is not blocking, then how do I know when it's done? [duplicate]

I have created a custom layout which extends ViewGroup. Everything is working fine and I am getting the layout as expected.
I want to dynamically change an element in the layout. However this is not working as I am calling it in onCreate and till that time the entire layout is not actually (drawn) inflated onto the screen and hence do not have actual size.
Is there any event which can be used to find out when the inflation of the layout is done? I tried onFinishInflate but that would not work as Viewgroup has multiple views and this would be called multiple times.
I am thinking of creating an interface in the Custom layout class but not sure when to fire it?
If I understand your requirements correctly, an OnGlobalLayoutListener may give you what you need.
View myView=findViewById(R.id.myView);
myView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//At this point the layout is complete and the
//dimensions of myView and any child views are known.
}
});
Usually when creating a custom layout extending View or ViewGroup, you have to override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) and protected void onLayout(boolean changed, int left, int top, int right, int bottom). These are called during the process of inflation in order to obtain the size and location information related to the view. Also, subsequently, if you are extending ViewGroup you are to call measure(int widthMeasureSpec, int heightMeasureSpec) and layout(int l, int t, int r, int b) on every child view contained within. (measure() is called in onMeasure() and layout() is called in onLayout()).
Anyway, in onMeasure(), you generally do something like this.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
// Gather this view's specs that were passed to it
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int chosenWidth = DEFAULT_WIDTH;
int chosenHeight = DEFAULT_HEIGHT;
if(widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY)
chosenWidth = widthSize;
if(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.EXACTLY)
chosenHeight = heightSize;
setMeasuredDimension(chosenWidth, chosenHeight);
*** NOW YOU KNOW THE DIMENSIONS OF THE LAYOUT ***
}
In onLayout() you get the actual pixel coordinates of the View, so you can get the physical size like so:
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
// Android coordinate system starts from the top-left
int width = right - left;
int height = bottom - top;
}

My ImageView size has been changed

I've got a MainActivity, and I've got a custom TouchView object within the MainActivity which extends ImageView. No XML layout is used in my project, I have the following lines in the onCreate() of MainActivity:
...
myTouchView = new TouchView();
myTouchView.layout(0,0,bitmap.getWidth(),bitmap.getHeight());//bitmap W*H: 3264*2448
MainActivity.this.setContentView(myTouchView);
...
I then set a breakpoint in the onDraw() of TouchView and there I always get the width and height of TouchView object which are not 3264*2448 which I've set on layout(). This means the size of my TouchView has been changed before onDraw(). The width and height I got are 480*320(screen size in fact).
How can I get the correct size(3264*2448)?
Thanks!
I've killed this question myself. I override onMeasure() like this:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
if(parentWidth == bmp.getWidth() && parentHeight == bmp.getHeight())
super.onMeasure(idthMeasureSpec, heightMeasureSpec);
else
this.setMeasuredDimension(parentWidth/2, parentHeight);
}
Works well for me, any potential bug.

Authorative way to override onMeasure()?

What's the correct way of overriding onMeasure()? I've seen various approaches. For example, Professional Android Development uses MeasureSpec to calculate the dimensions, then ends with a call to setMeasuredDimension(). For example:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth/2, parentHeight);
}
On the other hand, as per this post, the "correct" way is to use MeasureSpec, call setMeasuredDimensions(), followed by a call to setLayoutParams(), and ending with a call to super.onMeasure(). For example:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth/2, parentHeight);
this.setLayoutParams(new *ParentLayoutType*.LayoutParams(parentWidth/2,parentHeight));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
So which is the right way? Neither approach has worked 100% for me.
I guess really what I'm asking is does anyone know of a tutorial that explains onMeasure(), layout, dimensions of child views etc.?
The other solutions are not comprehensive. They may work in some cases, and are a good place to start, but they may are not guaranteed to work.
When onMeasure gets called you may or may not have the rights to change the size. The values that are passed to your onMeasure (widthMeasureSpec, heightMeasureSpec) contain information about what your child view is allowed to do. Currently there are three values:
MeasureSpec.UNSPECIFIED - You can be as big as you'd like
MeasureSpec.AT_MOST - As big as you want (up to the spec size), This is parentWidth in your example.
MeasureSpec.EXACTLY - No choice. Parent has chosen.
This is done so that Android can make multiple passes to find the right size for each item, see here for more details.
If you do not follow these rules, your approach is not guaranteed to work.
For example if you want to check if you're allowed to change the size at all you can do the following:
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
Using this information you will know whether you can modify the values as in your code. Or if you are required to do something different. A quick and easy way to resolve your desired size is to use one of the following methods:
int resolveSizeAndState (int size, int measureSpec, int childMeasuredState)
int resolveSize (int size, int measureSpec)
While the first is only available on Honeycomb, the second is available on all versions.
Note: You may find that resizeWidth or resizeHeight are always false. I found this to be the case if I was requesting MATCH_PARENT. I was able to fix this by requesting WRAP_CONTENT on my parent layout and then during the UNSPECIFIED phase requesting a size of Integer.MAX_VALUE. Doing so gives you the max size your parent allows on the next pass through onMeasure.
The documentation is the authority on this matter: http://developer.android.com/guide/topics/ui/how-android-draws.html and http://developer.android.com/guide/topics/ui/custom-components.html
To summarize: at the end of your overridden onMeasure method you should call setMeasuredDimension.
You should not call super.onMeasure after calling setMeasuredDimension, that will just erase whatever you set. In some situations you might want to call the super.onMeasure first and then modify the results by calling setMeasuredDimension.
Don't call setLayoutParams in onMeasure. Layout happens in a second pass after measuring.
I think it depends on the parent which you are overriding.
For example, if you are extending a ViewGroup (like FrameLayout), when you have measured the size, you should call like below
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
because you may want to ViewGroup to do rest work (do some stuffs on child view)
If you are extending a View (like ImageView), you can just call this.setMeasuredDimension(width, height);, because the parent class will just do something like you have done usually.
In a word, if you want some features your parent class offers free you should call super.onMeasure() (pass MeasureSpec.EXACTLY mode measure spec usually), otherwise call this.setMeasuredDimension(width, height); is enough.
If changing the views size inside of onMeasure all you need is the setMeasuredDimension call. If you are changing the size outside of onMeasure you need to call setLayoutParams. For instance changing the size of a text view when the text is changed.
Depends on the control you are using. The instructions in the documentation work for some controls (TextView, Button, ...), but not for others (LinearLayout, ...). The way that worked very well for me was to call the super once I am done. Based on the article in the below link.
http://humptydevelopers.blogspot.in/2013/05/android-view-overriding-onmeasure.html
here is how I solved the problem:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....
setMeasuredDimension( measuredWidth, measuredHeight );
widthMeasureSpec = MeasureSpec.makeMeasureSpec( measuredWidth, MeasureSpec.EXACTLY );
heightMeasureSpec = MeasureSpec.makeMeasureSpec( measuredHeight, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Moreover it was necessary for the ViewPager component
I guess, setLayoutParams and recalculating the measurements is a workaround to resize the child views correctly, as this is usually done in the derived class's onMeasure.
However, this rarely works correct (for whatever reason...), better invoke measureChildren (when deriving a ViewGroup) or try something similar when necessary.
you can take this piece of code as an example of onMeasure()::
public class MyLayerLayout extends RelativeLayout {
public MyLayerLayout(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int currentChildCount = getChildCount();
for (int i = 0; i < currentChildCount; i++) {
View currentChild = getChildAt(i);
//code to find information
int widthPercent = currentChildInfo.getWidth();
int heightPercent = currentChildInfo.getHeight();
//considering we will pass height & width as percentage
int myWidth = (int) Math.round(parentWidth * (widthPercent / 100.0));
int myHeight = (int) Math.round(parentHeight * (heightPercent / 100.0));
//Considering we need to set horizontal & vertical position of the view in parent
AlignmentTraitValue vAlign = currentChildInfo.getVerticalLocation() != null ? currentChildlayerInfo.getVerticalLocation() : currentChildAlignmentTraitValue.TOP;
AlignmentTraitValue hAlign = currentChildInfo.getHorizontalLocation() != null ? currentChildlayerInfo.getHorizontalLocation() : currentChildAlignmentTraitValue.LEFT;
int topPadding = 0;
int leftPadding = 0;
if (vAlign.equals(currentChildAlignmentTraitValue.CENTER)) {
topPadding = (parentHeight - myHeight) / 2;
} else if (vAlign.equals(currentChildAlignmentTraitValue.BOTTOM)) {
topPadding = parentHeight - myHeight;
}
if (hAlign.equals(currentChildAlignmentTraitValue.CENTER)) {
leftPadding = (parentWidth - myWidth) / 2;
} else if (hAlign.equals(currentChildAlignmentTraitValue.RIGHT)) {
leftPadding = parentWidth - myWidth;
}
LayoutParams myLayoutParams = new LayoutParams(myWidth, myHeight);
currentChildLayoutParams.setMargins(leftPadding, topPadding, 0, 0);
currentChild.setLayoutParams(myLayoutParams);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

Custom Viewgroup works inside LinearLayout not RelativeLayout

I wanted to create a custom LinearLayout (and later a custom ImageButton) that could take percentage values for both dimensions of size based on its parent's size regardless of the parent type (Relative or Linear). I was following this post: How to size an Android view based on its parent's dimensions, and it was very helpful, but I have a problem that those answers don't address.
When I place my Custom LinearLayout inside another LinearLayout, everything works as expected. My Custom LinearLayout covers the expected space (80% of the parent's width in the example below).
However if I place it inside a RelativeLayout, my screen always shows empty, I am not sure why this happens.
Here is my class:
public class ButtonPanel extends LinearLayout {
public ButtonPanel(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int newWidth = (int) Math.ceil(parentWidth * 0.8);
this.setMeasuredDimension(newWidth, parentHeight);
this.setLayoutParams(new RelativeLayout.LayoutParams(newWidth,parentHeight));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
And here is my testing layout for the activity.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.android.tests.views.ButtonPanel
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#drawable/inner_panel"
android:gravity="center_horizontal"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true">
</com.android.tests.views.ButtonPanel>
</RelativeLayout>
In my activity all I do is set the Content View to the above layout.
(Incidentally, does anybody now how I could get the type of the parent dynamically for setting the new LayoutParameters? Above you'll see the parent type (RelativeLayout) hard-coded into the Custom View onMeasure function)
Thanks in advance for any help!
Is this exposed to be a problem?
this.setLayoutParams(new RelativeLayout.LayoutParams(newWidth,parentHeight)); // <-- a RelativeLayout params?
In the onMeasure function you could use something like this to know what class is the parent of the view.
this.getParent().getClass().getName()
This should also work
a instanceof B
or
B.class.isAssignableFrom(a.getClass())
When using "instanceof", you need to know the class of "B" at compile time. When using "isAssignableFrom" it can be dynamic and change during runtime.
If you are not compfortable with string comparison, you could also use enums.
Turns out my two inquiries in this post were more related than expected.
I realized that by setting my view's LayoutParams to a completely new instance, I was overwriting the layout positioning information needed by the Relative Layout to position my view.
By 'zeroing out' that information, my view has the right dimensions, but the layout doesn't know where to place it, so it simply doesn't.
The following code for the new onMeasure shows how just directly modifying the height and width of the LayoutParams already attached to my view I avoid both overwriting the layout position information and having to create new LayoutParams based on the parent's type.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int specWidth = MeasureSpec.getSize(widthMeasureSpec);
int specHeight = MeasureSpec.getSize(heightMeasureSpec);
int newWidth = (int) Math.ceil(parentWidth * 0.8);
int newHeight = (int) Math.ceil(parentHeight * 0.8);
this.setMeasuredDimension(newWidth, newHeight);
this.getLayoutParams().height = newHeight;
this.getLayoutParams().width = newWidth;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Now, I'll be honest and say that this code is still not bug-free. Bringing the activity to the foreground and background multiple times constantly reduces the size of this custom view. The 0.8 reduction factor gets applied over and over each time the activity is brought up (I suspect the setting of the LayoutParams has to do with it, it might actually be unnecessary, but I haven't has time to test).
BUT, this still answered the question concerning this post, namely, why was my view not appearing at all despite having the right dimensions.

Categories

Resources