I want to design user interface like Google location history.
I had to replicate this UI for an application I worked on using RecyclerView.
Every row is a horizontal LinearLayout which contains the icon, the line and the views at the right. The line is a FrameLayout with a rounded background and the semi transparent circles are Views.
Because there is no space between rows the single pieces of the line appear joined.
The item layout looks like this:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- the circular icon on the left -->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_place"
android:tint="#android:color/white"
android:layout_marginRight="24dp"
android:padding="4dp"
android:background="#drawable/circle_bg"/>
<!-- the blue line -->
<FrameLayout
android:layout_width="15dp"
android:layout_height="match_parent"
android:padding="2dp"
android:id="#+id/item_line">
<!-- the semi transparent circle on the line -->
<View
android:layout_width="11dp"
android:layout_height="11dp"
android:background="#drawable/circle"/>
</FrameLayout>
<!-- views at the right of the blue line -->
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingBottom="32dp"
android:clickable="true"
android:background="?android:attr/selectableItemBackground">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="#style/TextAppearance.AppCompat.Title"
android:id="#+id/item_title"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/item_subtitle"/>
<!-- other views -->
</LinearLayout>
</LinearLayout>
A convenient way to render differently the line's caps for the top one, the middle ones and the last is to use position-related itemViewTypes in the Adapter:
private static final int VIEW_TYPE_TOP = 0;
private static final int VIEW_TYPE_MIDDLE = 1;
private static final int VIEW_TYPE_BOTTOM = 2;
private List<Item> mItems;
// ...
class ViewHolder extends RecyclerView.ViewHolder {
TextView mItemTitle;
TextView mItemSubtitle;
FrameLayout mItemLine;
public ViewHolder(View itemView) {
super(itemView);
mItemTitle = (TextView) itemView.findViewById(R.id.item_title);
mItemSubtitle = (TextView) itemView.findViewById(R.id.item_subtitle);
mItemLine = (FrameLayout) itemView.findViewById(R.id.item_line);
}
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
Item item = mItems.get(position);
// Populate views...
switch(holder.getItemViewType()) {
case VIEW_TYPE_TOP:
// The top of the line has to be rounded
holder.mItemLine.setBackground(R.drawable.line_bg_top);
break;
case VIEW_TYPE_MIDDLE:
// Only the color could be enough
// but a drawable can be used to make the cap rounded also here
holder.mItemLine.setBackground(R.drawable.line_bg_middle);
break;
case VIEW_TYPE_BOTTOM:
holder.mItemLine.setBackground(R.drawable.line_bg_bottom);
break;
}
}
#Override
public int getItemViewType(int position) {
if(position == 0) {
return VIEW_TYPE_TOP;
else if(position == mItems.size() - 1) {
return VIEW_TYPE_BOTTOM;
}
return VIEW_TYPE_MIDDLE;
}
The background drawables can be defined like this:
<!-- line_bg_top.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#color/colorPrimary"/>
<corners
android:topLeftRadius="15dp"
android:topRightRadius="15dp"/>
<!-- this has to be at least half of line FrameLayout's
width to appear completely rounded -->
</shape>
<!-- line_bg_middle.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#color/colorPrimary"/>
</shape>
<!-- line_bg_bottom.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#color/colorPrimary"/>
<corners
android:bottomLeftRadius="15dp"
android:bottomRightRadius="15dp"/>
</shape>
Of course you can also use ListView or if you know that the steps will always be just a few, a simple vertical LinearLayout will be enough.
Sadly the Google Maps Android app is not open source, it's not that easy to know the official way so... Material Design interpretations!
Related
I would like to have a Listview in which the rows:
have rounded corners;
are partially colored (the left part of each row);
contain a shape in their colored area which is filled with another color.
Here is an example from another app:
I currently have two leads but I did not succeed with any of them.
My first lead (shape)
I found how to get rounded corners by using a shape and use it as a background for the Listview.
The drawable shape file "rounded_rectangle.xml":
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners
android:bottomRightRadius="7dp"
android:bottomLeftRadius="7dp"
android:topLeftRadius="7dp"
android:topRightRadius="7dp"/>
</shape>
The activity file:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent">
// ...
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="#+id/splitView"
android:id="#+id/lv_player"
android:layout_alignParentTop="true"
android:background="#drawable/rounded_rectangle"/>
// ...
</FrameLayout>
However, I don't know how to use a shape to have a row partially colored or draw an icon. I can fill the shape with a color but then the rows are completely colored.
My second lead (vector)
I also found that I can use a vector as a background of my Listview. The advantage is that I can draw whatever I want using paths.
An example of a vector file:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="64dp"
android:width="64dp"
android:viewportHeight="600"
android:viewportWidth="600" >
<path
android:name="w"
android:fillColor="#000000"
android:pathData="m 290,103 l-200,0 c -50,0 -50,-100 0,-100 l200,0 z"
/>
</vector>
I can easily create a path for the colored area of my rows and another for the icon. However, now the problem is that I don't know how to scale the size of the vector to the size of the line.
Do you know what would be the best option and how can I obtain the expected result?
Thanks in advance.
Consider using a CardView, it will help you to get the rounded corners. Inside the CardView create a RelativeLayout in order to get the rest of the stuff you've mentioned above.
Here is a good tutorial : Using the CardView | CodePath Android Cliffnotes
First of all you need to create your fancy item separately like having xml layout contain the description of the custom item :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="#+id/optionName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_weight="10"
android:fontFamily="sans-serif-medium"
android:text="#string/option"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#android:color/white"
android:textStyle="bold" />
<CheckBox
android:id="#+id/activated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center" />
</LinearLayout>
than create your adapter that will make your list have your layout as an item
public class ListOptionsAdapter extends BaseAdapter {
List<OptionsObject> Features = new ArrayList<OptionsObject>();
LayoutInflater inflater;
Context context;
public ListOptionsAdapter(List<OptionsObject> features, Context context) {
super();
Features = features;
inflater = LayoutInflater.from(context);
this.context = context;
}
#Override
public int getCount() {
return Features.size();
}
#Override
public OptionsObject getItem(int position) {
return Features.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.item_list_view, parent, false);
holder = new ViewHolder();
holder.optionName = (TextView) convertView.findViewById(R.id.optionName);
holder.isActivate = (CheckBox) convertView.findViewById(R.id.activated);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
return convertView;
}
static class ViewHolder {
TextView optionName;
CheckBox isActivate;
}
}
after that as above i specified the name of the layout i will use as item in the method getView() and affect every element to the view holder
NB : in getView you have to specify the values for the elements that they will be displayed or leave them blank if u dont need
after that simply in my activity class i call my listview and my adapter
OptionList = (ListView) findViewById(R.id.optionList);
adapter = new ListOptionsAdapter(Features, getApplicationContext(), this);
OptionList.setAdapter(adapter);
I have a ListView and created a xml file for ListView items.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/list_item_selector_back"
android:minHeight="?attr/listPreferredItemHeight"
android:orientation="vertical">
<CheckedTextView
android:id="#+id/list_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:textColor="#color/list_item_selector_fore" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="#+id/list_item_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/list_item_selector_fore" />
<!-- and many more TextView elements -->
</LinearLayout>
</LinearLayout>
list_item_selector_fore.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#android:color/white" android:state_pressed="true" />
<item android:color="#android:color/white" android:state_activated="true" />
<item android:color="#android:color/white" android:state_focused="true" />
<item android:color="#android:color/black" />
</selector>
As you can see, I defined the background once in the root LinearLayout. But I had to set android:textColor="#color/list_item_selector_fore" for every TextView.
I tried it with foreground, but nothing happened. How can I get this smarter?
HOW TO LOOK:
two list view items
If you want do it for all TextView, the best way should be to subclass TextView
public class CustomeTextView extends TextView{
public CustomeTextView(Context context) {
super(context);
this.setTextColor(R.color.white);
}
}
If I understand the question correctly you want to change the colour of the text inside a TextView. From your XML it seems you want it to be onClick therefore you can do this:
TextView textView = (TextView) findViewById(R.id.myTextView);
textView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
textView.setTextColor('set text color here');
}
});
EDIT:
If you have these items inside of a ListView I would suggest that the easiest way for you to achieve what you want is to set an OnItemClickListenerand then change the textColor based on the OnItemClick event, which may look something like this;
ListView listView = (ListView) findViewById(R.id.listView);
listView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TextView textView = (TextView) view.findViewById(R.id.rowItemTextView);
textView.setTextColor('color goes here');
}
});
Hopefully this short snippet will guide you in the right direction to go.
I am looking for a custom widget to draw a circle with multiple border colors.
Say for example if my total circle represent 0-360, I need to color my circle border with different colors.
For example, I need to mark 0-60 with red, 61-120 with green, 121-300 with magenta and 301-360 with yellow border color.
please suggest me how I can do it in android.
You application is pretty simple. I don't recommend your using an external library. You can quickly implement a class that draws and manages your desired shape. An example is presented:
public class DifferentColorCircularBorder{
private RelativeLayout parentLayout;
public DifferentColorCircularBorder(RelativeLayout parentLayout) {
this.parentLayout = parentLayout;
}
public void addBorderPortion(Context context, int color, int startDegree, int endDegree) {
ProgressBar portion = getBorderPortion(context, color, startDegree, endDegree);
parentLayout.addView(portion);
}
private ProgressBar getBorderPortion(Context context, int color, int startDegree, int endDegree) {
LayoutInflater inflater = LayoutInflater.from(context);
ProgressBar portion = (ProgressBar) inflater.inflate(R.layout.border_portion, parentLayout, false);
portion.setRotation(startDegree);
portion.setProgress(endDegree - startDegree);
portion.getProgressDrawable().setColorFilter(color, Mode.SRC_ATOP);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) portion.getLayoutParams();
params.addRule(RelativeLayout.CENTER_IN_PARENT);
portion.setLayoutParams(params);
return portion;
}
}
border_portion is defined as below:
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="220dp"
android:layout_height="220dp"
android:progressDrawable="#drawable/circle_exterior"
android:layout_centerInParent="true"
android:max="360"/>
circle_exterior is defined here:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadius="100dp"
android:thickness="10dp" >
<solid android:color="#ff111111" />
</shape>
The MainActivity class is defined like this:
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RelativeLayout interiorLayout = (RelativeLayout) findViewById(R.id.interior);
DifferentColorCircularBorder border = new DifferentColorCircularBorder(interiorLayout);
border.addBorderPortion(getApplicationContext(), Color.RED, 0, 40);
border.addBorderPortion(getApplicationContext(), Color.GREEN, 40, 90);
border.addBorderPortion(getApplicationContext(), Color.BLUE, 90, 270);
border.addBorderPortion(getApplicationContext(), 0xFF123456, 270, 360);
}
}
finally activity_main layout is:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
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" >
<RelativeLayout
android:id="#+id/interior"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<View
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#drawable/circle_interior_bg"
android:layout_centerInParent="true" />
</RelativeLayout>
</RelativeLayout>
Explanation about the dimensions: This is an example. Here, I have picked the dimensions to fit the circle perfectly. Change these based on your application.
Image sample:
i just created a simple Library for that purpose CircularStatusView , it was inspired by WhatsApp Status and it's easy to use.
first up add the view, in my case i've added it around CircleImageView but you can use on any view.
<RelativeLayout
android:id="#+id/image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="75dp"
android:layout_height="75dp"
android:layout_centerInParent="true"
android:padding="6dp"
android:src="#mipmap/ic_launcher" />
<com.devlomi.circularstatusview.CircularStatusView
android:id="#+id/circular_status_view"
android:layout_width="75dp"
android:layout_height="75dp"
android:layout_centerInParent="true"
app:portion_color="#color/colorAccent"
app:portion_spacing="4dp"
app:portion_width="4dp"
app:portions_count="8" />
</RelativeLayout>
you can set the portions count Programmatically by using:
circularStatusView.setPortionsCount(count);
and for the portions color:
circularStatusView.setPortionsColor(color);
you can also set specific color for every portion:
circularStatusView.setPortionColorForIndex(/*index of portions starting from first portion at the top CW */ i, color);
for this you can try this library that i had come across
https://github.com/mucahitsidimi/GaugeView might be useful.
uses a custom view of fixed lengths to render the circle by using canvas
I've been experimenting with the ripple animation in my latest side project. I'm having some trouble finding an "elegant" solution to using it in certain situations for touch events. Namely with images, especially in list, grid, and recycle views. The animation almost always seems to animate behind the view, not the on top of it. This is a none issue in Buttons and TextViews but if you have a GridView of images, the ripple appears behind or below the actual image. Obviously this is not what I want, and while there are solutions that I consider to be a work around, I'm hoping there is something simpler i'm just unaware of.
I use the following code to achieve a custom grid view with images. I'll give full code CLICK HERE so you can follow along if you choose.
Now just the important stuff. In order to get my image to animate on touch I need this
button_ripple.xml
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#color/cream_background">
<item>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Pressed -->
<item
android:drawable="#color/button_selected"
android:state_pressed="true"/>
<!-- Selected -->
<item
android:drawable="#color/button_selected"
android:state_selected="true"/>
<!-- Focus -->
<item
android:drawable="#color/button_selected"
android:state_focused="true"/>
<!-- Default -->
<item android:drawable="#color/transparent"/>
</selector>
</item>
</ripple>
custom_grid.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<ImageView
android:id="#+id/sceneGridItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/button_ripple"
android:gravity="center_horizontal"/>
</LinearLayout>
activity_main.xml
<GridView
android:id="#+id/sceneGrid"
android:layout_marginTop="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:verticalSpacing="15dp"
android:numColumns="5" />
The line where all magic and problems occur is when I set the background. While this does in fact give me a ripple animation on my imageview, it animates behind the imageview. I want the animation to appear on top of the image. So I tried a few different things like
Setting the entire grid background to button_ripple.
<GridView
android:id="#+id/sceneGrid"
android:layout_marginTop="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:verticalSpacing="15dp"
android:background="#drawable/button_ripple"
android:numColumns="5" />
It does exactly what you'd think, now the entire grid has a semi transparent background and no matter what image i press the entire grid animates from the center of the grid. While this is kind of cool, its not what I want.
Setting the root/parent background to button_ripple.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:background="#drawable/button_ripple"
android:orientation="horizontal">
The area is now larger and fills the entire cell of the grid (not just the image), however it doesn't bring it to the front.
Changing custom_grid.xml to a RelativeLayout and putting two ImageViews on top of each other
custom_grid.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="#+id/gridItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true" />
<ImageView
android:id="#+id/gridItemOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:background="#drawable/button_ripple" />
</RelativeLayout>
CustomGridAdapter.java
....
gridItemOverLay = (ImageView) findViewById(R.id.gridItemOverlay);
gridItemOverlay.bringToFront();
This works. Now the bottom ImageView contains my image, and the top animates, giving the illusion of a ripple animation on top of my image. Honestly though this is a work around. I feel like this is not how it was intended. So I ask you fine people, is there a better way or even a different way?
I liked android developer's answer so I decided to investigate how to do step 2 of his solution in code.
You need to get this piece of code from Jake Wharton here : https://gist.github.com/JakeWharton/0a251d67649305d84e8a
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
public class ForegroundImageView extends ImageView {
private Drawable foreground;
public ForegroundImageView(Context context) {
this(context, null);
}
public ForegroundImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundImageView);
Drawable foreground = a.getDrawable(R.styleable.ForegroundImageView_android_foreground);
if (foreground != null) {
setForeground(foreground);
}
a.recycle();
}
/**
* Supply a drawable resource that is to be rendered on top of all of the child
* views in the frame layout.
*
* #param drawableResId The drawable resource to be drawn on top of the children.
*/
public void setForegroundResource(int drawableResId) {
setForeground(getContext().getResources().getDrawable(drawableResId));
}
/**
* Supply a Drawable that is to be rendered on top of all of the child
* views in the frame layout.
*
* #param drawable The Drawable to be drawn on top of the children.
*/
public void setForeground(Drawable drawable) {
if (foreground == drawable) {
return;
}
if (foreground != null) {
foreground.setCallback(null);
unscheduleDrawable(foreground);
}
foreground = drawable;
if (drawable != null) {
drawable.setCallback(this);
if (drawable.isStateful()) {
drawable.setState(getDrawableState());
}
}
requestLayout();
invalidate();
}
#Override protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || who == foreground;
}
#Override public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (foreground != null) foreground.jumpToCurrentState();
}
#Override protected void drawableStateChanged() {
super.drawableStateChanged();
if (foreground != null && foreground.isStateful()) {
foreground.setState(getDrawableState());
}
}
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (foreground != null) {
foreground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
invalidate();
}
}
#Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (foreground != null) {
foreground.setBounds(0, 0, w, h);
invalidate();
}
}
#Override public void draw(Canvas canvas) {
super.draw(canvas);
if (foreground != null) {
foreground.draw(canvas);
}
}
}
This is the attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ForegroundImageView">
<attr name="android:foreground"/>
</declare-styleable>
</resources>
Now, create your ForegroundImageView like so in your layout.xml:
<com.example.ripples.ForegroundImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:foreground="?android:selectableItemBackground"
android:src="#drawable/apples"
android:id="#+id/image" />
The image will now ripple.
Instead of trying to add the ripple to each individual view in the adapter, you can just add it at the GridView level like this:
<GridView
android:id="#+id/gridview"
...
android:drawSelectorOnTop="true"
android:listSelector="#drawable/your_ripple_drawable"/>
Maybe try one of these solutions (got the tip from here) :
Wrap the drawable in a RippleDrawable² before setting it on the ImageView:
Drawable image = …
RippleDrawable rippledImage = new RippleDrawable(
ColorStateList.valueOf(rippleColor), image, null);
imageView.setImageDrawable(rippledImage);
Extend ImageView and add a foreground attribute to it (like FrameLayout has³). See this example⁴ from +Chris Banes of adding it to a LinearLayout. If you do this then make sure you pass through the touch co-ordinates so that the ripple starts from the correct point:
#Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (foreground != null) {
foreground.setHotspot(x, y);
}
}
I have an image gallery (built with a RecyclerView and a GridLayoutManager). The image is set using Picasso. To add a ripple to the image I've wrapped the ImageView with a FrameLayout. The item layout is:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="120dp"
android:foreground="#drawable/ripple"
android:clickable="true"
android:focusable="true"
>
<ImageView
android:id="#+id/grid_item_imageView"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_gravity="center"
android:scaleType="centerInside"
/>
</FrameLayout>
Note the android:foreground. It's not the same as android:background. I've tried without android:clickable="true" and android:focusable="true" and it also works, but it doesn't hurt.
Then add a ripple.xml drawable into res/drawable:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#7FF5AC8E" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#android:color/transparent" />
</shape>
</item>
</selector>
Note that this shows a semi-transparent color on top of the image when the item is selected (for devices < 5.0). You can remove it if you don't want it.
Then add the ripple.xml drawable with ripple into res/drawable-v21:
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#color/coral">
</ripple>
You can use the default ripple effect instead of the custom ripple.xml, but it's really difficult to see on top of an image because it's grey:
android:foreground="?android:attr/selectableItemBackground"
I have a ListView, where each item has a custom LinearLayout with a bg image, a textView and 2 imageViews.
Now I need that while the user is touching the item, all of those switch to the "pressed" state:
the bg image of the LiearLayout must be replaced with another one
the TextView should change textColor
both ImageViews in the item should switch to alternative images
Normally such stuff would be done using an xml resource with selector inside, e.g. the LinearLayout would use a drawable with selector inside for background, the TextView a drawable with selector and colors for textColor, and ImageViews use selector with images inside for src.
The problem is that the pressed state is only detected by the LinearLayout and not by the child views (?), so only the background image changes.
I've tried implementing this using OnTouchListener, but then comes the problem that I can't securely get access to Views inside the list item.
I tried caching the view which I return in getView() of the list item to then later change the images and text color. This works usually, but e.g. if one of the list items opens another activity, then the view somehow gets lost and the highlighted state stays indefinitely. I've tried debugging and it works correctly if I step thru with the debugger.
Also, reusing the cachedView seems to bring no good and messes things up completely, so I'm just inflating a new view for the list item each time (this must be inefficient).
Just in case, here is the code of the custom list item item i'm using for the custom list adapter:
public class MyListItem extends AbstractListItem
{
private int iconResource, iconHighlightedResource;
private int textResource;
private View.OnClickListener onClickListener;
private LinearLayout currentView;
private ImageView imgIcon;
private TextView txtText;
private ImageView imgArrow;
private boolean bIsHighlighted;
public MyListItem(int iconResource, int iconHighlightedResource, int textResource, View.OnClickListener onClickListener)
{
this.iconResource = iconResource;
this.iconHighlightedResource = iconHighlightedResource;
this.textResource = textResource;
this.onClickListener = onClickListener;
}
public View getView(View cachedView)
{
this.currentView = buildView();
populateView();
update();
return this.currentView;
}
private LinearLayout buildView()
{
LayoutInflater inflater = (LayoutInflater)App.get().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return (LinearLayout)inflater.inflate(R.layout.my_menu_item, null);
}
private void populateView()
{
this.imgIcon = (ImageView)this.currentView.findViewById(R.id.img_menu_item_icon);
this.txtText = (TextView)this.currentView.findViewById(R.id.txt_menu_item_text);
this.txtText.setText(this.textResource);
this.txtText.setTypeface(App.fontCommon);
this.imgArrow = (ImageView)this.currentView.findViewById(R.id.img_menu_item_arrow);
this.currentView.setOnClickListener(this.onClickListener);
this.currentView.setOnTouchListener(this.highlighter);
}
private View.OnTouchListener highlighter = new View.OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event)
{
int nAction = event.getAction();
int nActionCode = nAction & MotionEvent.ACTION_MASK;
switch (nActionCode)
{
case MotionEvent.ACTION_DOWN:
bIsHighlighted = true;
update();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
bIsHighlighted = false;
update();
break;
}
return false;
}
};
private void update()
{
if (this.bIsHighlighted)
{
updateForHighlightedState();
}
else
{
updateForNormalState();
}
}
private void updateForHighlightedState()
{
Resources r = App.get().getResources();
this.currentView.setBackgroundResource(R.drawable.button_beveled_m_call_to_action_taking_input);
this.imgIcon.setImageResource(this.iconHighlightedResource);
this.txtText.setTextColor(r.getColor(R.color.white));
this.imgArrow.setImageResource(R.drawable.arrow_highlighted);
}
private void updateForNormalState()
{
Resources r = App.get().getResources();
this.currentView.setBackgroundColor(r.getColor(R.color.white));
this.imgIcon.setImageResource(this.iconResource);
this.txtText.setTextColor(r.getColor(R.color.text_dark));
this.imgArrow.setImageResource(R.drawable.arrow);
}
}
Here is the layout file (xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#color/white"
android:gravity="center_vertical"
android:padding="5dp" >
<ImageView
android:id="#+id/img_menu_item_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="#drawable/info" />
<TextView
android:id="#+id/txt_menu_item_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="24dp"
android:text="Menu item"
android:textColor="#color/text_dark"
android:layout_marginLeft="5dp" />
<ImageView
android:id="#+id/img_menu_item_arrow"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="#drawable/arrow" />
</LinearLayout>
After lots of experimenting finally this worked:
Every child view inside the list item layout must have android:duplicateParentState="true".
Then all of them can just use selector drawables. No extra effort inside the code is required.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#drawable/my_item_bg"
android:gravity="center_vertical"
android:padding="5dp" >
<ImageView
android:id="#+id/img_menu_item_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="#drawable/selector_info"
android:duplicateParentState="true" />
<TextView
android:id="#+id/txt_menu_item_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="24dp"
android:text="Menu item"
android:textColor="#drawable/selector_color_my_button_text"
android:layout_marginLeft="5dp"
android:duplicateParentState="true" />
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="#drawable/selector_arrow"
android:duplicateParentState="true" />
</LinearLayout>
You should create custom drawable selectors, and set them as the background to your listview element.
Step#1, create another layout (named: layout_selected for this example), with the appropriate background color for your pressed state (like the layout file you supplied, but with the background attribute of the linear set to another color).
Then you will define a drawable selector, which will be placed in your drawable folder), defining which background should be use in which instance. This will look something like this:
<!-- pressed -->
<item android:drawable="#drawable/layout_selected" android:state_pressed="true"/>
<!-- focused -->
<item android:drawable="#drawable/layout_normal" android:state_focused="true"/>
<!-- default -->
<item android:drawable="#drawable/layout_normal"/>
Finally, to use this in your list, when you set the layout for your adapter, set it to the selector we just created, instead of your standard layout.
Maybe a little hard to explain, but you want to use 'Drawable Selectors' to accomplish what you want.
I would suggest to add ViewHolder pattern for listview. This will optimize your listview drawing & creating UI.
Also in that we can use setTag to save instance of row. In that you can handle touch event.