/* Code snipet from the MainActivity */
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* Linear layout */
LinearLayout layout = (LinearLayout)findViewById(R.id.main_container);
/* Creating two similar custom views */
CustomView row1 = new CustomView(this); /* First row */
CustomView row2 = new CustomView(this); /* second row */
layout.add(row1,0);
layout.add(row2,1);
}
CustomView.java
public class CustomView extends View {
public final String TAG = "CustomView";
Context context;
private Drawable backgroundDrawable;
Rect backGroundDrawableRect;
public TextMessageView(Context context) {
super(context);
this.context = context;
backgroundDrawable = ContextCompat.getDrawable(context,R.drawable.background_drawable);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
backGroundDrawableRect.left = left;
backGroundDrawableRect.right = right;
backGroundDrawableRect.bottom = bottom;
backGroundDrawableRect.top = top;
backgroundDrawable.setBounds(backGroundDrawableRect);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/* 100 pixel height for the view */
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),100);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(backgroundDrawable != null) {
backgroundDrawable.draw(canvas);
}
}
}
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="#+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
The problem is that the view added in the second row of the linear layout is not getting displayed. During debugging, when i enabled the 'show layout bound ' in developers option , I found that the view in the second row was taking its required space but the content which is a drawable was not getting displayed.
The Canvas coordinates are automatically adjusted to the view's position so you need to set the left and top bounds of your Drawable to zero instead:
backGroundDrawableRect.left = 0;
backGroundDrawableRect.right = right - left;
backGroundDrawableRect.bottom = bottom - top;
backGroundDrawableRect.top = 0;
Change in CustomView
public class CustomView extends View {
public final String TAG = "CustomView";
Context context;
private Drawable backgroundDrawable;
Rect backGroundDrawableRect;
public CustomView(Context context) {
super(context);
this.context = context;
backgroundDrawable = ContextCompat.getDrawable(context,R.drawable.ic_launcher);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
/* backGroundDrawableRect.left = left;
backGroundDrawableRect.right = right;
backGroundDrawableRect.bottom = bottom;
backGroundDrawableRect.top = top;
backgroundDrawable.setBounds(backGroundDrawableRect);
*/
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/* 100 pixel height for the view */
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),100);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(backgroundDrawable != null) {
backgroundDrawable.draw(canvas);
}
}
and your launcher activity do this change
public class Test extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test1);
/* Linear layout */
LinearLayout layout = (LinearLayout) findViewById(R.id.main_container);
/* Creating two similar custom views */
CustomView row1 = new CustomView(getApplicationContext()); /* First row */
row1.setBackgroundColor(Color.RED);
CustomView row2 = new CustomView(getApplicationContext()); /* Second row */
row2.setBackgroundColor(Color.GREEN);
layout.addView(row1);
layout.addView(row2);
}
}
Related
Background
I have a custom ViewGroup subclass that rotates and mirrors its child view. The purpose for this is to correctly display traditional Mongolian text.
I could put anything is this ViewGroup, but for my current project I am putting an EditText in it. (I was never successful in just rotating and mirroring the EditText directly. However, wrapping it in this custom view group does work.)
Problem
My problem is that when I try to resize the ViewGroup programmatically, its child view is not getting resized properly along with it. I would like the EditText to match the size of the parent ViewGroup so that it appears to be a single view.
MCVE
I made a new project to show the problem. The button increases the width of the ViewGroup (shown in red). The images show the project start (with everything working fine) and two width increments. The EditText is white and is not getting resized even though the width and height are set to match_parent
The full project code is below.
MongolViewGroup.java (Custom ViewGroup that rotates and mirrors its content)
public class MongolViewGroup extends ViewGroup {
private int angle = 90;
private final Matrix rotateMatrix = new Matrix();
private final Rect viewRectRotated = new Rect();
private final RectF tempRectF1 = new RectF();
private final RectF tempRectF2 = new RectF();
private final float[] viewTouchPoint = new float[2];
private final float[] childTouchPoint = new float[2];
private boolean angleChanged = true;
public MongolViewGroup(Context context) {
this(context, null);
}
public MongolViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public View getView() {
return getChildAt(0);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View view = getView();
if (view != null) {
measureChild(view, heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(resolveSize(view.getMeasuredHeight(), widthMeasureSpec),
resolveSize(view.getMeasuredWidth(), heightMeasureSpec));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (angleChanged) {
final RectF layoutRect = tempRectF1;
final RectF layoutRectRotated = tempRectF2;
layoutRect.set(0, 0, right - left, bottom - top);
rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
rotateMatrix.postScale(-1, 1);
rotateMatrix.mapRect(layoutRectRotated, layoutRect);
layoutRectRotated.round(viewRectRotated);
angleChanged = false;
}
final View view = getView();
if (view != null) {
view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
viewRectRotated.bottom);
}
}
#Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.rotate(-angle, getWidth() / 2f, getHeight() / 2f);
canvas.scale(-1, 1);
super.dispatchDraw(canvas);
canvas.restore();
}
#Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidate();
return super.invalidateChildInParent(location, dirty);
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
viewTouchPoint[0] = event.getX();
viewTouchPoint[1] = event.getY();
rotateMatrix.mapPoints(childTouchPoint, viewTouchPoint);
event.setLocation(childTouchPoint[0], childTouchPoint[1]);
boolean result = super.dispatchTouchEvent(event);
event.setLocation(viewTouchPoint[0], viewTouchPoint[1]);
return result;
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
MongolViewGroup viewGroup;
EditText editText;
int newWidth = 300;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewGroup = (MongolViewGroup) findViewById(R.id.viewGroup);
editText = (EditText) findViewById(R.id.editText);
}
public void buttonClicked(View view) {
newWidth += 200;
ViewGroup.LayoutParams params = viewGroup.getLayoutParams();
params.width=newWidth;
viewGroup.setLayoutParams(params);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.example.mongolviewgrouptest.MainActivity">
<com.example.mongolviewgrouptest.MongolViewGroup
android:id="#+id/viewGroup"
android:layout_width="100dp"
android:layout_height="200dp"
android:background="#color/colorAccent">
<EditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="#android:color/black"
android:background="#android:color/white"/>
</com.example.mongolviewgrouptest.MongolViewGroup>
<Button
android:text="Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/button"
android:onClick="buttonClicked"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
You're not recalculating viewRectRotated for your EditText when the
ViewGroup's onLayout(...) method is called again.
Since angleChanged is set to false (and never changes) after your ViewGroups first layout, then the part that calculates the left, right, top and bottom values for your EditText
is skipped any time after the first time when your ViewGroup
requestsLayout (when you change its height or width).
As such, your EditText is still laid out with the same left,right,top
and bottom values it was initially laid out with.
Do away with the angleChanged and it should work just fine. Like so:
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final RectF layoutRect = tempRectF1;
final RectF layoutRectRotated = tempRectF2;
layoutRect.set(0, 0, right - left, bottom - top);
rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
rotateMatrix.postScale(-1, 1);
rotateMatrix.mapRect(layoutRectRotated, layoutRect);
layoutRectRotated.round(viewRectRotated);
final View view = getView();
if (view != null) {
view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
viewRectRotated.bottom);
}
}
I've tested this and it works just fine this way.
If you need angleChanged for any reason, then just make sure it's changed back to true inside your ViewGroup's onMeasure method so that viewRectRotated is recalculated again. However I wouldn't recommend that.
I want to create a custom view which should be added in another custom view.
The second view will be a container, so it should be able to contain the first view as its child.
For creating this views I am extending ViewGroup & LinearLayout classes.
Child view class is NodeView
public class NodeView extends LinearLayout
{
private final static String TAG = "NodeView";
private ImageView ivTop;
private ImageView ivBottom;
private Context myContext;
public NodeView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.myContext = context;
setOrientation(LinearLayout.VERTICAL);
setGravity(Gravity.CENTER);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.view_test_multi, this, true);
ivTop = (ImageView) getChildAt(0);
ivBottom = (ImageView) getChildAt(2);
ivTop.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v)
{
Toast.makeText(myContext, "Top Clicked", Toast.LENGTH_SHORT).show();
}
});
ivBottom.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v)
{
Toast.makeText(myContext, "Bottom Clicked", Toast.LENGTH_SHORT).show();
}
});
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
}
public NodeView(Context context)
{
this(context, null);
}
}
& the container class is TreeViewGroup
public class TreeViewGroup extends ViewGroup
{
private static final String TAG = "CustomTreeNodeView";
NodeView nodeView;
public TreeViewGroup(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
nodeView = new NodeView(getContext());
addView(nodeView);
}
public TreeViewGroup(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public TreeViewGroup(Context context)
{
this(context, null, 0);
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
}
}
& xml layout for node view is view_test_multi.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_centerVertical="true"
android:src="#drawable/point_grey" />
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:src="#drawable/point_red" />
<ImageView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_centerVertical="true"
android:src="#drawable/point_grey" />
</merge>
My activity's layout is activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res/com.ab1209.testcustom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<com.ab1209.testcustom.view.TreeViewGroup
android:id="#+id/activity_main_custom_tree_node_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
MainActivity class is
**public class MainActivity extends Activity
{
TreeViewGroup treeNodeView;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
treeNodeView = (TreeViewGroup) findViewById(R.id.activity_main_custom_tree_node_view);
}
}**
When I run the app I don't see the NodeView added in main View. Am I doing the right thing if not please tell me how can I make it working?
To create a custom ViewGroup, the only method you need to override is
onLayout. The onLayout is triggered after the ViewGroup itself has
finished laying itself out inside its own container ViewGroup and is
now responsible for laying out its children. It should call the layout
method on all of its children to now position and size them (the left
and top parameters will determine the child view’s x and y and the
right and bottom will determine its width (right – left) and height
(top-bottom).
So your TreeViewGroup code will look like :
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) child
.getLayoutParams();
int childLeft = 0;
int childTop = 0;
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 200; // Calculate the height
int measuredHeight = 200; // Calculate the width
setMeasuredDimension(measuredWidth, measuredHeight);
}
Refer this link http://arpitonline.com/2012/07/01/creating-custom-layouts-for-android/
I'm using a custom view MaskCustomView by subclassing View. The width/height layout params for the custom view are both WRAP_CONTENT.
What happens is the custom view in the LinearLayout takes all the space. I can see that using uiautomatorViewer :
Code is :
public class MaskImageView extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mask_image);
LinearLayout container = (LinearLayout) findViewById(R.id.maskID);
MaskCustomView maskView = new MaskCustomView(this);
maskView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
container.addView(maskView);
}
public class MaskCustomView extends View {
private Bitmap bp;
private Paint p;
public MaskCustomView(Context context) {
super(context);
bp = BitmapFactory.decodeResource(getResources(), R.drawable.apple);
p = new Paint();
}
#Override
public void onDraw(Canvas canvas) {
canvas.drawBitmap(bp, 0, 0 , p);
super.onDraw(canvas);
}
}
}
xml layout :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="#+id/maskID"
android:layout_width="match_parent"
android:layout_height="match_parent">
Shouldn't just wrap content and don't use whole space for both height and width ?
Try to make your MaskCustomView override onMeasure:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(bp == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
setMeasuredDimension(MeasureSpec.makeMeasureSpec(bp.getWidth(), MeasureSpec.MODE_CONSTANT), MeasureSpec.makeMeasureSpec(bp.getHeight(), MeasureSpec.MODE_CONSTANT);
}
}
In the view hierarchy:
Window > ViewGroup (root) > ViewGroup[...] > View (child)
I need to know root dimension in a profound child onMeasure event.
Exemple:
#Override
protected void onMeasure(int wMS, int hMS) {
int desiredW = Math.round(rootW * factorW);
int desiredH = Math.round(rootH * factorH);
/* ... compute final dimensions ... */
setMeasuredDimension(finalW, finalH);
}
Note: At this moment, getRootView and getWindow dimentions equals to 0 because children have to setMeasuredDimention before their parents
Considering a lot of children needing this dimension, to do it:
I created an interface:
public interface OnRootSizeChanged {
public void onRootSizeChanged(int w, int h);
}
I implemented my child which now implements OnRootSizeChanged inteface:
private int rootW;
private int rootH;
#Override
public void onRootSizeChanged(int w, int h) {
rootW = w;
rootH = h;
}
I implemented root view:
#Override
protected void onMeasure(int wMS, int hMS) {
int w = MeasureSpec.getSize(wMS);
int h = MeasureSpec.getSize(hMS);
dispatchOnRootSizeChange(this, w, h);
super.onMeasure(wMS, hMS);
}
private void dispatchOnRootSizeChange(ViewGroup v, int w, int h) {
for (int i = 0, n = v.getChildCount(); i < n; i++) {
View child = v.getChildAt(i);
if (child instanceof OnRootSizeChanged)
((OnRootSizeChanged) child).onRootSizeChanged(w, h);
if (child instanceof ViewGroup)
dispatchOnRootSizeChange((ViewGroup) child, w, h);
}
}
My question is:
Have I simpler way to do this without recursivity or with better practice ?
Update: This method is invalid in case of ViewPager element in ViewGroup[...] breabcrumb. When ViewPager instantiate children pages, they have not yet received OnRootSizeChanged event so:
Children have to ask the root dimension, no the root to tell his dimension to their children
So I searched how to target root from a profound child to ask him:
getRootView() not seems targeting the view attached with setContentView()
getWindow().getDecorView() either
One possible way is:
On child:
#Override
protected void onMeasure(int wMS, int hMS) {
ViewParent parent = getParent();
while (parent instanceof RootViewClass == false)
parent = parent.getParent();
RootViewClass root = (RootViewClass) parent;
int desiredW = Math.round(root.w * factorW);
int desiredH = Math.round(root.h * factorH);
/* ... compute final dimensions ... */
setMeasuredDimension(finalW, finalH);
}
On root instance of RootViewClass:
public int w, h;
#Override
protected void onMeasure(int wMS, int hMS) {
w = MeasureSpec.getSize(wMS);
h = MeasureSpec.getSize(hMS);
super.onMeasure(wMS, hMS);
}
But with lot of children, I don't think this is a good practice. If I could find root view without use of loop.
You can forward the parent's size by storing those values in the onMeasure() method as you receive them and then letting the children access the values in their onMeasure() method through the Context reference:
// simple interface
public interface ParentRef {
void YourViewGroup getRoot();
}
// the Activity implements the interface above
public class YourActivity extends Activity implements ParentRef {
private YourViewGroup mRoot;
//in onCreate initialize the mRoot reference
#Override
public YourViewGroup getRoot() {
return mRoot;
}
//... rest of the Activity
}
// the custom ViewGroup will store the dimensions:
//fields in the root view
private int mCurWidth;
private int mCurHeight;
#Override
protected void onMeasure(int wMS, int hMS) {
int w = MeasureSpec.getSize(wMS);
int h = MeasureSpec.getSize(hMS);
mCurWidth = w;
mCurHeight = h;
// now as the children are measured they can see the values above
super.onMeasure(wMS, hMS);
}
public int getStoredWidth() {
return mCurWidth;
}
public int getStoredHeight() {
return mCurHeight;
}
// in the children's onMeasure simply do:
#Override
protected void onMeasure(int wMS, int hMS) {
final YourViewGroup root = ((ParentRef) getContext()).getRoot();
//width root.getStoredWidth()
// height root.getStoredHeight()
/* ... compute final dimensions ... */
setMeasuredDimension(finalW, finalH);
}
You can use a ViewTreeObserver (http://developer.android.com/reference/android/view/ViewTreeObserver.html)
//...get your view, than attach a viewTreeObserver on it!
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//misure the view here, like view.getHeight()
}
});
I have a custom View, which extends RelativeLayout.
This Relative Layout should consist of Rectangles which extends View, and this Rectangles should be painted by a bitmap and have a TextView in it..
But i cant add this Rectangles to the RelativeLayout so that they are below of each other.. it seems that the LayoutParams do not work.. Can you help me please?
Custom View extends RelativeLayout
public class DrawView extends RelativeLayout {
private Context context;
public DrawView(Context context, ArrayList<String> dragFieldText, ArrayList<String> targetFieldText, int height, int width) {
super(context);
this.context = context;
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT);
Rectangle.count = 1;
for (int i = 0; i < targetFieldText.size(); i++) {
Rectangle targetRectangle = new Rectangle(context);
this.addView(targetRectangle);
}
Rectangle tmpRect = null;
for (int i = 0; i < dragFieldText.size(); i++) {
Rectangle dragRectangle = new Rectangle(context);
if(tmpRect != null){
tmpRect.setId(i);
lp.addRule(RelativeLayout.BELOW, tmpRect.getId());
dragRectangle.setLayoutParams(lp);
}
this.addView(dragRectangle);
tmpRect = dragRectangle;
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = View.MeasureSpec.getSize(widthMeasureSpec);
int height = View.MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
Rectangle extends View
public class Rectangle extends View{
public Rectangle(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(getSomeBitmapImg(), new Matrix(), null);
}