I'm getting an Illegal Argument Exception when running this Activity. Could someone point out why? My comment about the Specs will hopefully lead you to my misunderstanding of this code:
[Activity(Label = "Views/Layouts/GridLayout/3. Form (Java)", MainLauncher = true)]
[IntentFilter(new[] { Intent.ActionMain }, Categories = new string[] { })]
public class GridLayout3 : Activity {
protected override void OnCreate(Bundle bundle) {
base.OnCreate(bundle);
SetContentView(Grid(this));
//SetContentView (Create (this));
}
public static View Grid(Context context) {
GridLayout gL = new GridLayout(context);
gL.AlignmentMode = GridAlign.Bounds;
gL.UseDefaultMargins = true;
//gL.ColumnOrderPreserved = false;
//gL.RowOrderPreserved = false;
gL.RowCount = 5;
gL.ColumnCount = 5;
//I want these Specs to tell the View to take up 2 rows of space.
//IE, (row 0-indexed) of 3 and 4 in column 4 should contain
//the same button
GridLayout.Spec specialRowSpec = GridLayout.InvokeSpec(3, 4);
GridLayout.Spec specialColSpec = GridLayout.InvokeSpec(4);
bool keyIsSpecial = false;
for (int i = 0; i < gL.RowCount; i++) {
GridLayout.Spec row = GridLayout.InvokeSpec(i);
for (int v = 0; v < gL.ColumnCount; v++) {
GridLayout.Spec col = GridLayout.InvokeSpec(v);
Button bt = new Button(context);
bt.Text = "Tester";
if (i == 3 && v == 4) {
keyIsSpecial = true;
}
if (keyIsSpecial) {
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.RowSpec = specialRowSpec;
param.ColumnSpec = specialColSpec;
param.Height = 100;
gL.AddView(bt, new GridLayout.LayoutParams(param));
return gL;
} else {
gL.AddView(bt, new GridLayout.LayoutParams(row, col));
}
if (v == gL.ColumnCount - 1) {
v = 0;
break;
}
}
}
return gL;
}
The exception is thrown when I add the view with the special Specs: gL.AddView(bt, new GridLayout.LayoutParams(param));
Here's the link to the GridLayout.Spec documentation that I may be misunderstanding: http://developer.android.com/reference/android/widget/GridLayout.Spec.html
I've also tried exchanging the second argument to my specialRowSpec to use total size (height) instead of the index of the row. For example: GridLayout.Spec specialRowSpec = GridLayout.InvokeSpec(3, 2); This just gives me a 5 by 5 grid of buttons with no buttons spanning multiple rows.
Any questions?
Related
I have a constraintLayout that contains multiple nodeView's. A nodeView is a ImageView line attached to the left side of a ImageView circle
I now want to connect X amount of nodes together. To programmatically set constraints, you use the R.id, but since I'm connecting multiple same nodes together, and they all share the same R.id, this isn't working. Is there any way to reference a specific view's ImageView as a reference for setting a constraint for another ImageView? I'm starting to think I'm approaching this the wrong way entirely. Thanks.
EDIT: Here is the rest of the code.
node code
private void init(Context context, AttributeSet attrs, String description, boolean active, boolean base) {
View inflatedView = inflate(context, R.layout.tracking_node, this);
nodeLine = inflatedView.findViewById(R.id.imageNodeLine);
nodeImage = inflatedView.findViewById(R.id.imageNode);
nodeText = inflatedView.findViewById(R.id.textNode);
nodeLine.setId(View.generateViewId());
nodeImage.setId(View.generateViewId());
nodeText.setText(description);
if (active){
nodeImage.setImageResource(R.drawable.circle_green);
nodeLine.setImageResource(R.color.support_success);
}else{
nodeImage.setImageResource(R.drawable.circle_grey);
nodeImage.setImageResource(R.color.grey);
}
//Remove left-side connecting line if base node
if (base){
nodeLine.getLayoutParams().width = 20;
nodeLine.setImageResource(R.color.transparent);
}
}
public int getNodeImageId(){
return nodeImage.getId();
}
public int getNodeLineId(){
return nodeLine.getId();
}
constraintLayout code
private void init(Context context, AttributeSet attrs) {
View inflatedView = inflate(context, R.layout.delivery_status_view, this);
deliveryTrackerView = inflatedView.findViewById(R.id.linearLayoutDeliveryTracking);
shippingDetailsButton = inflatedView.findViewById(R.id.btnShippingDetails);
//steps[] is a string array that contains the content of each node
DeliveryNodeView node = new DeliveryNodeView(context, attrs, steps[0], true, true);
//Saves resource ID of last node image
int pastNodeID = node.getNodeImageId();
//Generates nodes
for (int i = 1; i < steps.length; i++){
boolean active = ((i + 1) / currentStep) <= 1;
node = new DeliveryNodeView(context, attrs, steps[i], active, false);
int nodeLineID = node.getNodeLineId();
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(deliveryTrackerView);
deliveryTrackerView.addView(node);
constraintSet.connect(nodeLineID, ConstraintSet.START, pastNodeID, ConstraintSet.END);
pastNodeID = node.getNodeImageId();
}
}
There are a few problems with your code. Here is some sample code that builds a 5x5 colored box array that looks like this:
The comments in the code outline the key steps. activity_main.xml is just an empty ConstraintLayout.
MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ConstraintLayout layout = findViewById(R.id.layout);
int colorCounter = 0;
int idToTop = ConstraintSet.PARENT_ID;
int idToTopSide = ConstraintSet.TOP;
for (int i = 0; i < 5; i++) {
int idToLeft = ConstraintSet.PARENT_ID;
int idToLeftSide = ConstraintSet.START;
for (int j = 0; j < 5; j++) {
View box = getBox(colorCounter++ % 2 == 0);
// Add the view before getting the ConstraintSet.
layout.addView(box);
ConstraintSet cs = new ConstraintSet();
cs.clone(layout);
// Must constrain the view horizontally...
cs.connect(box.getId(), ConstraintSet.START, idToLeft, idToLeftSide);
//... and vertically.
cs.connect(box.getId(), ConstraintSet.TOP, idToTop, idToTopSide);
idToLeft = box.getId();
idToLeftSide = ConstraintSet.END;
// Apply the ConstraintSet to the layout.
cs.applyTo(layout);
}
idToTop = idToLeft;
idToTopSide = ConstraintSet.BOTTOM;
}
}
private View getBox(boolean isRed) {
View view = new View(this);
view.setId(View.generateViewId());
view.setBackgroundColor((isRed) ? Color.RED : Color.BLUE);
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(200, 200);
view.setLayoutParams(lp);
return view;
}
}
Alternate code with the same result that separates out the view creation from making of the ConstraintSet connections. This may be a little more efficient.
MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ConstraintLayout layout = findViewById(R.id.layout);
int colorCounter = 0;
int[][] connections = new int[5][5];
for (int row = 0; row < 5; row++) {
for (int col = 0; col < 5; col++) {
View box = getBox(colorCounter++ % 2 == 0);
// Add the view before getting the ConstraintSet.
layout.addView(box);
connections[row][col] = box.getId();
}
}
int idToTop = ConstraintSet.PARENT_ID;
int idToTopSide = ConstraintSet.TOP;
ConstraintSet cs = new ConstraintSet();
cs.clone(layout);
for (int i = 0; i < 5; i++) {
cs.connect(connections[i][0], ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START);
cs.connect(connections[i][0], ConstraintSet.TOP, idToTop, idToTopSide);
for (int j = 1; j < 5; j++) {
// Must constrain the view horizontally...
cs.connect(connections[i][j], ConstraintSet.START, connections[i][j - 1], ConstraintSet.END);
//... and vertically.
cs.connect(connections[i][j], ConstraintSet.TOP, idToTop, idToTopSide);
// Apply the ConstraintSet to the layout.
}
idToTop = connections[i][0];
idToTopSide = ConstraintSet.BOTTOM;
}
cs.applyTo(layout);
}
private View getBox(boolean isRed) {
View view = new View(this);
view.setId(View.generateViewId());
view.setBackgroundColor((isRed) ? Color.RED : Color.BLUE);
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(200, 200);
view.setLayoutParams(lp);
return view;
}
}
in Android Studio I developed a view that consist of 15 TableRows, and each TableRow contains 15 ImageViews. (Each ImageView store a Bitmap with square shape, and this produce a 15 x 15 square matrix). Relevant coding of View onCreate roughly as below:
TableLayout tableLayout = (TableLayout) findViewById(R.id.tableLayout);
TableRow[] tr = new TableRow[15];
for (int i = 0; i < 15; i++) {
tr[i] = new TableRow(this);
for (int j = 0; j < 15; j++) {
bmp = BitmapFactory.decodeResource(getResources(),R.drawable.jmpty);
imageViews[j] = new ImageView(this);
imageViews[j].setImageBitmap(bmp);
imageViews[j].setOnClickListener(new doSomething());
tr[i].addView(imageViews[j],oParams);
}
tableLayout.addView(tr[i]);
}
....
My question is how do I know which imageView (Total = 15 x 15 = 225) was being clicked by user by utilizing OnClick event? i.e. If the user click the ImageView located at (imageViews[13] located at TableRow=4), can I return capture this x-y coordinate information (e.g. x=13, y=4) and return to other parts of program?
private class doSomething implements View.OnClickListener {
#Override
public void onClick(View v) {
ImageView iv = (ImageView) v;
......
// what I want:
int x = iv.GetImageViewNumber(); // how can I do this???
int y = iv.GetTableRowNumber(); // how can I do this???
Or is there any other better alternative way of doing this?
You can create a class who inherits from ImageView, and inside this class create 2 attributes int x and int y, So when onClickListener is called you should cast the View to your custom class.
Example:
public calss MyCustomImageView extends ImageView{
public int x;
public int y;
}
TableLayout tableLayout = (TableLayout) findViewById(R.id.tableLayout);
TableRow[] tr = new TableRow[15];
for (int i = 0; i < 15; i++) {
tr[i] = new TableRow(this);
for (int j = 0; j < 15; j++) {
bmp = BitmapFactory.decodeResource(getResources(),R.drawable.jmpty);
imageViews[j] = new MyCustomImageView(this);
imageViews[j].setImageBitmap(bmp);
imageViews[j].setOnClickListener(new doSomething());
imageViews[j].x = i;
imageViews[j].y = j;
tr[i].addView(imageViews[j],oParams);
}
tableLayout.addView(tr[i]);
}
#Override
public void onClick(View v) {
MyCustomImageView iv = (MyCustomImageView) v;
int x = iv.x;
int y = iv.y;
}
I hope it will help you, sorry for my English I'm beginner!
to know which imageview has been clicked, you need to identify that view by its ID or Tag. You need to set it in your for loop and then get it when u want to identify in onClick method.
String tagString = "Image";
for (int i = 0; i < 15; i++) {
tr[i] = new TableRow(this);
for (int j = 0; j < 15; j++) {
bmp = BitmapFactory.decodeResource(getResources(),R.drawable.jmpty);
imageViews[j] = new ImageView(this);
imageViews[j].setImageBitmap(bmp);
//setting image tag for each image in "Image1", "Image2"... format
imageViews[j].setTag(tagString+j);
imageViews[j].setOnClickListener(new doSomething());
tr[i].addView(imageViews[j],oParams);
}
tableLayout.addView(tr[i]);
}
Identify the image in your onClick method
#Override
public void onClick(View v) {
Toast.makeText(this, "Clicked items is "+v.getTag().toString(), Toast.LENGTH_SHORT).show();;
}
I have been trying to make a gridview with drag and drop functionality along with one cell of different size. I have already made the the grid drag and drop and its working fine. you can check the code from here
but I want it to be like this and purely dynamic as I will be draging and dropping the other which will be replaced and resized automatically
Updated with new code that accommodates resizing of cells.
Your question refers to GridView but the code you supplied doesn't mention GridView but uses GridLayout instead, so I am assuming that GridLayout is the right layout.
I have put together a demo using a mocked-up layout with one 2x2 tile. I have modified the code that you have supplied to accommodate the 2x2 tile. Other than the code that I added to implement the 2x2 tile, the only other change to MainAcitivity was to the calculateNextIndex method that uses a different way of calculating the index at an (x, y) position. Layouts and the LongPressListener class were also mocked up since they were not supplied.
Here is a video of the demo:
MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final int ITEMS = 10;
private GridLayout mGrid;
private ScrollView mScrollView;
private ValueAnimator mAnimator;
private Boolean isScroll = false;
private GridLayout.Spec m1xSpec = GridLayout.spec(GridLayout.UNDEFINED, 1);
private GridLayout.Spec m2xSpec = GridLayout.spec(GridLayout.UNDEFINED, 2);
private int mBaseWidth;
private int mBaseHeight;
private int mBaseMargin;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mScrollView = (ScrollView) findViewById(R.id.scrollView);
mScrollView.setSmoothScrollingEnabled(true);
mGrid = (GridLayout) findViewById(R.id.grid);
mGrid.setOnDragListener(new DragListener());
final LayoutInflater inflater = LayoutInflater.from(this);
GridLayout.LayoutParams lp;
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
float dpiToPx = displayMetrics.density;
View view = inflater.inflate(R.layout.item, mGrid, false);
lp = (GridLayout.LayoutParams) view.getLayoutParams();
mBaseWidth = lp.width;
mBaseHeight = lp.height;
mBaseMargin = lp.rightMargin;
for (int i = 0; i < ITEMS; i++) {
final View itemView = inflater.inflate(R.layout.item, mGrid, false);
final TextView text = (TextView) itemView.findViewById(R.id.text);
text.setText(String.valueOf(i + 1));
itemView.setOnLongClickListener(new LongPressListener());
lp = (i == 0) ? make2x2LayoutParams(itemView) : make1x1LayoutParams(itemView);
mGrid.addView(itemView, lp);
}
}
private GridLayout.LayoutParams make2x2LayoutParams(View view) {
GridLayout.LayoutParams lp = (GridLayout.LayoutParams) view.getLayoutParams();
lp.width = mBaseWidth * 2 + 2 * mBaseMargin;
lp.height = mBaseHeight * 2 + 2 * mBaseMargin;
lp.rowSpec = m2xSpec;
lp.columnSpec = m2xSpec;
lp.setMargins(mBaseMargin, mBaseMargin, mBaseMargin, mBaseMargin);
return lp;
}
private GridLayout.LayoutParams make1x1LayoutParams(View view) {
GridLayout.LayoutParams lp = (GridLayout.LayoutParams) view.getLayoutParams();
lp.width = mBaseWidth;
lp.height = mBaseHeight;
lp.setMargins(mBaseMargin, mBaseMargin, mBaseMargin, mBaseMargin);
lp.rowSpec = m1xSpec;
lp.columnSpec = m1xSpec;
return lp;
}
private int mDraggedIndex;
class DragListener implements View.OnDragListener {
#Override
public boolean onDrag(View v, DragEvent event) {
final View view = (View) event.getLocalState();
int index = calculateNextIndex(event.getX(), event.getY());
View child;
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
mDraggedIndex = index;
break;
case DragEvent.ACTION_DRAG_LOCATION:
if (view == v) return true;
// get the new list index
final Rect rect = new Rect();
mScrollView.getHitRect(rect);
final int scrollY = mScrollView.getScrollY();
if (event.getY() - scrollY > mScrollView.getBottom() - 250) {
startScrolling(scrollY, mGrid.getHeight());
} else if (event.getY() - scrollY < mScrollView.getTop() + 250) {
startScrolling(scrollY, 0);
} else {
stopScrolling();
}
child = mGrid.getChildAt(0);
if (index == 0) {
child.setLayoutParams(make1x1LayoutParams(child));
view.setLayoutParams(make2x2LayoutParams(view));
} else if (mDraggedIndex == 0) {
view.setLayoutParams(make1x1LayoutParams(view));
child.setLayoutParams(make2x2LayoutParams(child));
} else {
child.setLayoutParams(make2x2LayoutParams(child));
view.setLayoutParams(make1x1LayoutParams(view));
}
mGrid.removeView(view);
mGrid.addView(view, index);
break;
case DragEvent.ACTION_DROP:
for (int i = 0; i < mGrid.getChildCount(); i++) {
child = mGrid.getChildAt(i);
child.setLayoutParams(make1x1LayoutParams(child));
}
mGrid.removeView(view);
if (index == 0) {
view.setLayoutParams(make2x2LayoutParams(view));
}
mGrid.addView(view, index);
view.setVisibility(View.VISIBLE);
mGrid.getChildAt(0).setLayoutParams(make2x2LayoutParams(mGrid.getChildAt(0)));
break;
case DragEvent.ACTION_DRAG_ENDED:
if (!event.getResult()) {
view.setVisibility(View.VISIBLE);
}
break;
}
return true;
}
}
private void startScrolling(int from, int to) {
if (from != to && mAnimator == null) {
isScroll = true;
mAnimator = new ValueAnimator();
mAnimator.setInterpolator(new OvershootInterpolator());
mAnimator.setDuration(Math.abs(to - from));
mAnimator.setIntValues(from, to);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mScrollView.smoothScrollTo(0, (int) valueAnimator.getAnimatedValue());
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
isScroll = false;
mAnimator = null;
}
});
mAnimator.start();
}
}
private void stopScrolling() {
if (mAnimator != null) {
mAnimator.cancel();
}
}
private int calculateNextIndexOld(float x, float y) {
// calculate which column to move to
final float cellWidth = mGrid.getWidth() / mGrid.getColumnCount();
final int column = (int) (x / cellWidth);
final float cellHeight = mGrid.getHeight() / mGrid.getRowCount();
final int row = (int) Math.floor(y / cellHeight);
int index = row * mGrid.getColumnCount() + column;
if (index >= mGrid.getChildCount()) {
index = mGrid.getChildCount() - 1;
}
Log.d("MainActivity", "<<<<index=" + index);
return index;
}
private int calculateNextIndex(float x, float y) {
// calculate which column to move to
int index;
for (index = 0; index < mGrid.getChildCount(); index++) {
View child = mGrid.getChildAt(index);
Rect rect = new Rect();
child.getHitRect(rect);
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
break;
}
}
if (index >= mGrid.getChildCount()) {
// Move into empty cell? Calculate based upon uniform cell sizes.
index = calculateNextIndexOld(x, y);
}
if (index >= mGrid.getChildCount()) {
// Can't determine where to put it? Add it to the end.
index = mGrid.getChildCount() - 1;
}
return index;
}
}
If you work with the demo a little, you will see that it is possible to move tiles such that a 1x1 tile gap is opened up. This may be OK, but the code may need to be reworked a little if not.
You can try :
https://github.com/askerov/DynamicGrid
I hope it can help your problem!
I am working in Android studio and using TableLayout. i want to display small imageButtons. and height of each button can be changed programmatically. I tried my best. but does not change the height of imageButton, Please anyone help me, what changes can i make in this code. thanks
public void settable() {
index = 0;
LinearLayout le = (LinearLayout) findViewById(R.id.onofffbutton);
float height = le.getHeight();
height= height/9.0f;
for (int a = 0; a < maxrow; a++) {
TableRow row = new TableRow(this);
for (int b = 0; b < col; b++) {
location[a][b] = new ImageView(this);
/*LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(50, 50);
location[a][b].setLayoutParams(layoutParams);*/
if (isOn[a][b]) {
location[a][b].setBackgroundResource(R.drawable.box1);
} else {
location[a][b].setBackgroundResource(R.drawable.box);
}
location[a][b].setMaxHeight(height);
location[a][b].setOnClickListener(Onclick);
location[a][b].setId(index++);
row.addView(location[a][b]);
}
tableLayout.addView(row, a);
}
}
I am absolutely bigenner in android programming. I have done so far like this..
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scorecard_fragment, container,
false);
gridLayout = (GridLayout) view.findViewById(R.id.GridLayout1);
/*** Default first row */
gridLayout.setColumnCount(col);
gridLayout.setRowCount(row);
if (col == 1) {
defaultColumn();
}
else {
extendedColumn("");
}
return view;
}
public void defaultColumn() {
// Log.d(TAG, "Default col");
int i;
int k = 1;
for (i = 0; i < 20; i++) {
if (i == 0)
{
ImageView oImageView = new ImageView(getActivity());
oImageView.setImageResource(R.drawable.ic_player);
GridLayout.LayoutParams param = new
GridLayout.LayoutParams();
param.setMargins(0, 0, 0, 0);
gridLayout.addView(oImageView);
} else if (i < 19) {
TextView oTextView = new TextView(getActivity());
oTextView.append("" + (k++) + "\n");
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.setGravity(Gravity.CENTER);
param.setMargins(15, 15, 15, 15);
oTextView.setLayoutParams(param);
gridLayout.addView(oTextView);
} else {
TextView oTextView = new TextView(getActivity());
oTextView.append("\n");
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.setGravity(Gravity.CENTER);
param.setMargins(15, 15, 15, 15);
oTextView.setLayoutParams(param);
gridLayout.addView(oTextView);
}
}
col++;
}
public void extendedColumn(String str) {
Log.d("", "value c1" + col);
int i;
for (i = 0; i < 20; i++) {
if (i == 0)
{
TextView oTextView = new TextView(getActivity());
oTextView.setText(str);
gridLayout.addView(oTextView);
// oTextView.setBackgroundResource(R.drawable.border);
oTextView.setTextSize(14);
}
else if (i < 19) {
Log.d("Implement spinner", "sdfsdfsdf");
final Spinner s1 = new Spinner(getActivity());
final ArrayAdapter<CharSequence> adapter = ArrayAdapter
.createFromResource(getActivity(), R.array.score_array,
android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
s1.setAdapter(adapter);
s1.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> arg0, View arg1,
int arg2, long arg3) {
updateResult();
}
#Override
public void onNothingSelected(AdapterView<?> arg0) {
}
});
gridLayout.addView(s1);
}
else {
TextView oTextView = new TextView(getActivity());
oTextView.append("\n");
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.setGravity(Gravity.CENTER);
param.setMargins(15, 15, 15, 15);
oTextView.setLayoutParams(param);
gridLayout.addView(oTextView);
}
}
col++;
}
public void updateResult() {
int j = gridLayout.getColumnCount();
Log.d(" Column count", "" + j);
int k = gridLayout.getRowCount();
Log.d(" Row count", "" + k);
}
As you can see I am trying get the value from spinner and its current row and column value. I can do that using gridLayout.getColumnCount() and gridLayout.getRowCount(). But that gives me always the total column and row depend on how many user I have. But I want to get the specific row and column no matter how many user I have so that I can calculate my result by knowing which row and column has been selected by which user..Helps or suggestion will be highly appreciated.
so that I can calculate my result by knowing which value has been selected
To only know the selected value, just implement the onItemClick() method, and inside you will have position of selected value, the view, the parent ViewGroup, etc ...