This question already has answers here:
How to get text on an ActionBar Icon?
(4 answers)
Closed 4 years ago.
I have a menu item in the action bar. Along with the menu item image, I need to show some number associated with it which will change often. I am not using Action bar sherlock. I don't want to use that. Other than this everything else just works fine. In the shown image, the white icon color icon is mine. I need to generate the number with the red color background dynamically. How can I do that in Android?
Here is the sample image:
Update:
I have this menu item in my menu.xml. This should work like a notification menu item which shows the number of notification count. I set the menu icon like,
menuItem.setIcon(image);
Now, on top of the menu item I need to place one text view which has the total count of notifications.
is it possible to implement this functionality with viewbadger?
Github url
I discovered how to add an actionView to a menu item and retrieve as set values to the view in code.
See here: https://stackoverflow.com/a/16648170/857681
After a lot of trying of nearly all resources on SO I turned to blogs; succesfully. I want to share what worked for me (Api >= 13); source.
Let's start with the sweet code, the way it's used:
public boolean onCreateOptionsMenu(Menu menu) {
//inflate menu
getMenuInflater().inflate(R.menu.menu_my, menu);
// Get the notifications MenuItem and LayerDrawable (layer-list)
MenuItem item = menu.findItem(R.id.action_notifications);
LayerDrawable icon = (LayerDrawable) item.getIcon();
// Update LayerDrawable's BadgeDrawable
Utils2.setBadgeCount(this, icon, 2);
return true;
}
The menu_my.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="#+id/action_notifications"
android:icon="#drawable/ic_menu_notifications"
android:title="Notifications"
app:showAsAction="always" />
</menu>
This class that conveniently makes a BadgeDrawable; its appearance can be modified as well:
public class BadgeDrawable extends Drawable {
private float mTextSize;
private Paint mBadgePaint;
private Paint mTextPaint;
private Rect mTxtRect = new Rect();
private String mCount = "";
private boolean mWillDraw = false;
public BadgeDrawable(Context context) {
//mTextSize = context.getResources().getDimension(R.dimen.badge_text_size);
mTextSize = 12F;
mBadgePaint = new Paint();
mBadgePaint.setColor(Color.RED);
mBadgePaint.setAntiAlias(true);
mBadgePaint.setStyle(Paint.Style.FILL);
mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTypeface(Typeface.DEFAULT_BOLD);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
#Override
public void draw(Canvas canvas) {
if (!mWillDraw) {
return;
}
Rect bounds = getBounds();
float width = bounds.right - bounds.left;
float height = bounds.bottom - bounds.top;
// Position the badge in the top-right quadrant of the icon.
float radius = ((Math.min(width, height) / 2) - 1) / 2;
float centerX = width - radius - 1;
float centerY = radius + 1;
// Draw badge circle.
canvas.drawCircle(centerX, centerY, radius, mBadgePaint);
// Draw badge count text inside the circle.
mTextPaint.getTextBounds(mCount, 0, mCount.length(), mTxtRect);
float textHeight = mTxtRect.bottom - mTxtRect.top;
float textY = centerY + (textHeight / 2f);
canvas.drawText(mCount, centerX, textY, mTextPaint);
}
/*
Sets the count (i.e notifications) to display.
*/
public void setCount(int count) {
mCount = Integer.toString(count);
// Only draw a badge if there are notifications.
mWillDraw = count > 0;
invalidateSelf();
}
#Override
public void setAlpha(int alpha) {
// do nothing
}
#Override
public void setColorFilter(ColorFilter cf) {
// do nothing
}
#Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
}
This class that helps to set the number. I recommend implementing even more thods to set badge as date, etc:
public class Utils2 {
public static void setBadgeCount(Context context, LayerDrawable icon, int count) {
BadgeDrawable badge;
// Reuse drawable if possible
Drawable reuse = icon.findDrawableByLayerId(R.id.ic_badge);
if (reuse != null && reuse instanceof BadgeDrawable) {
badge = (BadgeDrawable) reuse;
} else {
badge = new BadgeDrawable(context);
}
badge.setCount(count);
icon.mutate();
icon.setDrawableByLayerId(R.id.ic_badge, badge);
}
}
And mui importante a drawable (like a layout) in res/drawable:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/ic_notification"
android:drawable="#drawable/ice_skate"
android:gravity="center" />
<!-- set a place holder Drawable so android:drawable isn't null -->
<item
android:id="#+id/ic_badge"
android:drawable="#drawable/ice_skate" />
</layer-list>
Good lucks!
Here is one thing you can try:
Create a custom Drawable that paint you image in the background and text on top of the image. Check out this post for sample.
Then set this Drawable as the MenuItem background dynamically...
Use action view. It works with both: default ActionBar and ActionBarSherlock.
Here is an example
With this approach you can just create your own View (by inflating some layout for example) and then do whatever you want (change background, change content, add another views dynamically if your action view is subclass of ViewGroup etc.).
Related
Whenever extending the class, the bar will be drawn in the vertical center of its container. I need to place text above the bar and increasing the height (I am setting an arbitrary height on the view) causes a bunch of blank space beneath the bar. I've been googling for quite some time now but all I find is regarding the thumb mostly, and I need to draw the bar on the bottom of its container.
This is what I'm trying to achieve as a single view (notice how the bar's position in not centered):
The only "useful" code I could find was this:
Window window = ((Activity) getContext()).getWindow();
window.setGravity(Gravity.BOTTOM);
WindowManager.LayoutParams wmlp = window.getAttributes();
wmlp.y = getHeight() / 4;
window.setAttributes(wmlp);
But it has no effect whatsoever.
My custom class, nothing out of the ordinary:
public class RateSeekbar extends AppCompatSeekBar {
private Paint lettersPaint;
private Paint numberPaint;
// constructors and call to init()
private void init() {
lettersPaint = new Paint();
numberPaint = new Paint();
... // style the two different paints
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // get a couple of strings from resources and measure them
int middleHor = getWidth() / 2;
canvas.drawText(text1, getPaddingLeft(), 40, lettersPaint);
canvas.drawText(text2, getWidth() - text2Size - getPaddingRight(), 40, lettersPaint);
canvas.drawText(String.valueOf(getProgress()), middleHor - (text3Size / 2), 50, numberPaint);
}
}
Any ideas?
I am making a reading app and it has a full screen activity.
When user selects the part of the text a contextual action bar appears with option of copy. This is the default behaviour. But this actionbar blocks the text under it so user cannot select it.
I want to show a popup window like below.
I tried returning false from onCreateActionMode but when i do this i can't select the text either.
I want to know if there is a standart way to achieve this as many reading applications use this design.
I don't know how Play Books achieves this, but you could create a PopupWindow and calculate where to position it based on the selected text using Layout.getSelectionPath and a little bit of math. Basically, we're going to:
Calculate the bounds of the selected text
Calculate the bounds and initial location of the PopupWindow
Calculate the difference between the two
Offset the PopupWindow to rest center horizontally/vertically above or below the selected text
Calculating the selection bounds
From the docs:
Fills in the specified Path with a representation of a highlight
between the specified offsets. This will often be a rectangle or a
potentially discontinuous set of rectangles. If the start and end are
the same, the returned path is empty.
So, the specified offsets in our case would be the start and end of the selection, which can be found using Selection.getSelectionStart and Selection.getSelectionEnd. For convenience, TextView gives us TextView.getSelectionStart, TextView.getSelectionEnd and TextView.getLayout.
final Path selDest = new Path();
final RectF selBounds = new RectF();
final Rect outBounds = new Rect();
// Calculate the selection start and end offset
final int selStart = yourTextView.getSelectionStart();
final int selEnd = yourTextView.getSelectionEnd();
final int min = Math.max(0, Math.min(selStart, selEnd));
final int max = Math.max(0, Math.max(selStart, selEnd));
// Calculate the selection outBounds
yourTextView.getLayout().getSelectionPath(min, max, selDest);
selDest.computeBounds(selBounds, true /* this param is ignored */);
selBounds.roundOut(outBounds);
Now that we have a Rect of the selected text bounds, we can choose where we want to place the PopupWindow relative to it. In this case, we'll center it horizontally along the top or bottom of the selected text, depending on how much space we have to display our popup.
Calculating the initial popup coordinates
Next we'll need to calculate the bounds of the popup content. To do this, we'll first need to call PopupWindow.showAtLocation, but the bounds of the View we inflate won't immediately be available, so I'd recommend using a ViewTreeObserver.OnGlobalLayoutListener to wait for them to become available.
popupWindow.showAtLocation(yourTextView, Gravity.TOP, 0, 0)
PopupWindow.showAtLocation requires:
A View to retrieve a valid Window token from, which just uniquely identifies the Window to place the popup in
An optional gravity, but in our case it'll be Gravity.TOP
Optional x/y offsets
Since we can't determine the x/y offset until the popup content is laid out, we'll just initially place it at the default location. If you try to call PopupWindow.showAtLocation before the View you pass in has been laid out, you'll receive a WindowManager.BadTokenException, so you may consider using a ViewTreeObserver.OnGlobalLayoutListener to avoid that, but it mostly comes up when you have text selected and rotate your device.
final Rect cframe = new Rect();
final int[] cloc = new int[2];
popupContent.getLocationOnScreen(cloc);
popupContent.getLocalVisibleRect(cbounds);
popupContent.getWindowVisibleDisplayFrame(cframe);
final int scrollY = ((View) yourTextView.getParent()).getScrollY();
final int[] tloc = new int[2];
yourTextView.getLocationInWindow(tloc);
final int startX = cloc[0] + cbounds.centerX();
final int startY = cloc[1] + cbounds.centerY() - (tloc[1] - cframe.top) - scrollY;
View.getLocationOnScreen will return us the x/y coordinates for the popup content.
View.getLocalVisibleRect will return us the bounds of the popup content
View.getWindowVisibleDisplayFrame will return us the offsets to accommodate for the action bar, if present
View.getScrollY will return us the y offset for whatever scroll container our TextView is in (ScrollView in my case)
View.getLocationInWindow will return us the y offset for our TextView, in case the action bar pushes it down a little
Once we've gotten all of the info we need, we can calculate the final starting x/y of the popup content and then use this to figure out the difference between them and the selected text Rect so we can PopupWindow.update to the new location.
Calculating the offset popup coordinates
// Calculate the top and bottom offset of the popup relative to the selection bounds
final int popupHeight = cbounds.height();
final int textPadding = yourTextView.getPaddingLeft();
final int topOffset = Math.round(selBounds.top - startY);
final int btmOffset = Math.round(selBounds.bottom - (startY - popupHeight));
// Calculate the x/y coordinates for the popup relative to the selection bounds
final int x = Math.round(selBounds.centerX() + textPadding - startX);
final int y = Math.round(selBounds.top - scrollY < startY ? btmOffset : topOffset);
If there's enough room to display the popup above the selected text, we'll put it there; otherwise, we'll offset it below the selected text. In my case, I have 16dp padding around my TextView, so that needs to be taken into account too. We'll end up with the final x and y location to offset the PopupWindow with.
popupWindow.update(x, y, -1, -1);
-1 here just represents the default width/height for we supplied for the PopupWindow, in our case it'll be ViewGroup.LayoutParams.WRAP_CONTENT
Listening for selection changes
We want the PopupWindow to update every time we change the selected text.
An easy way to listen for selection changes is to subclass TextView and provide a callback to TextView.onSelectionChanged.
public class NotifyingSelectionTextView extends AppCompatTextView {
private SelectionChangeListener listener;
public NotifyingSelectionTextView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (listener != null) {
if (hasSelection()) {
listener.onTextSelected();
} else {
listener.onTextUnselected();
}
}
}
public void setSelectionChangeListener(SelectionChangeListener listener) {
this.listener = listener;
}
public interface SelectionChangeListener {
void onTextSelected();
void onTextUnselected();
}
}
Listening for scroll changes
If you have a TextView in a scroll container like ScrollView, you may also want to listen for scroll changes so that you can anchor your popup while you're scrolling. An easy way to listen for those is to subclass ScrollView and provide a callback to View.onScrollChanged
public class NotifyingScrollView extends ScrollView {
private ScrollChangeListener listener;
public NotifyingScrollView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (listener != null) {
listener.onScrollChanged();
}
}
public void setScrollChangeListener(ScrollChangeListener listener) {
this.listener = listener;
}
public interface ScrollChangeListener {
void onScrollChanged();
}
}
Creating an empty ActionMode.Callback
Like you mention in your post, we'll need to return true in ActionMode.Callback.onCreateActionMode in order for our text to remain selectable. But we'll also need to call Menu.clear in ActionMode.Callback.onPrepareActionMode in order to remove all the items you may find in an ActionMode for selected text.
/** An {#link ActionMode.Callback} used to remove all action items from text selection */
static final class EmptyActionMode extends SimpleActionModeCallback {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Return true to ensure the text is still selectable
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Remove all action items to provide an actionmode-less selection
menu.clear();
return true;
}
}
Now we can use TextView.setCustomSelectionActionModeCallback to apply our custom ActionMode. SimpleActionModeCallback is a custom class that just provides stubs for ActionMode.Callback, kinda similar to ViewPager.SimpleOnPageChangeListener
public class SimpleActionModeCallback implements ActionMode.Callback {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
#Override
public void onDestroyActionMode(ActionMode mode) {
}
}
Layouts
This is the Activity layout we're using:
<your.package.name.NotifyingScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/notifying_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<your.package.name.NotifyingSelectionTextView
android:id="#+id/notifying_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textIsSelectable="true"
android:textSize="20sp" />
</your.package.name.NotifyingScrollView>
This is our popup layout:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/action_mode_popup_bg"
android:orientation="vertical"
tools:ignore="ContentDescription">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="#+id/view_action_mode_popup_add_note"
style="#style/ActionModePopupButton"
android:src="#drawable/ic_note_add_black_24dp" />
<ImageButton
android:id="#+id/view_action_mode_popup_translate"
style="#style/ActionModePopupButton"
android:src="#drawable/ic_translate_black_24dp" />
<ImageButton
android:id="#+id/view_action_mode_popup_search"
style="#style/ActionModePopupButton"
android:src="#drawable/ic_search_black_24dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="8dp"
android:background="#android:color/darker_gray" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="#+id/view_action_mode_popup_red"
style="#style/ActionModePopupSwatch"
android:src="#drawable/round_red" />
<ImageButton
android:id="#+id/view_action_mode_popup_yellow"
style="#style/ActionModePopupSwatch"
android:src="#drawable/round_yellow" />
<ImageButton
android:id="#+id/view_action_mode_popup_green"
style="#style/ActionModePopupSwatch"
android:src="#drawable/round_green" />
<ImageButton
android:id="#+id/view_action_mode_popup_blue"
style="#style/ActionModePopupSwatch"
android:src="#drawable/round_blue" />
<ImageButton
android:id="#+id/view_action_mode_popup_clear_format"
style="#style/ActionModePopupSwatch"
android:src="#drawable/ic_format_clear_black_24dp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
These are our popup button styles:
<style name="ActionModePopupButton">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:layout_weight">1</item>
<item name="android:background">?selectableItemBackground</item>
</style>
<style name="ActionModePopupSwatch" parent="ActionModePopupButton">
<item name="android:padding">12dp</item>
</style>
Util
The ViewUtils.onGlobalLayout you'll see is just a util method for handling some ViewTreeObserver.OnGlobalLayoutListener boilerplate.
public static void onGlobalLayout(final View view, final Runnable runnable) {
final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
runnable.run();
}
};
view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
}
Bringing it altogether
So, now that we've:
Calculated the selected text bounds
Calculated the popup bounds
Calculated the difference and determined the popup offsets
Provided a way to listen for scroll changes and selection changes
Created our Activity and popup layouts
Bringing everything together may look something like:
public class ActionModePopupActivity extends AppCompatActivity
implements ScrollChangeListener, SelectionChangeListener {
private static final int DEFAULT_WIDTH = -1;
private static final int DEFAULT_HEIGHT = -1;
private final Point currLoc = new Point();
private final Point startLoc = new Point();
private final Rect cbounds = new Rect();
private final PopupWindow popupWindow = new PopupWindow();
private final ActionMode.Callback emptyActionMode = new EmptyActionMode();
private NotifyingSelectionTextView yourTextView;
#SuppressLint("InflateParams")
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_action_mode_popup);
// Initialize the popup content, only add it to the Window once we've selected text
final LayoutInflater inflater = LayoutInflater.from(this);
popupWindow.setContentView(inflater.inflate(R.layout.view_action_mode_popup, null));
popupWindow.setWidth(WRAP_CONTENT);
popupWindow.setHeight(WRAP_CONTENT);
// Initialize to the NotifyingScrollView to observe scroll changes
final NotifyingScrollView scroll
= (NotifyingScrollView) findViewById(R.id.notifying_scroll_view);
scroll.setScrollChangeListener(this);
// Initialize the TextView to observe selection changes and provide an empty ActionMode
yourTextView = (NotifyingSelectionTextView) findViewById(R.id.notifying_text_view);
yourTextView.setText(IPSUM);
yourTextView.setSelectionChangeListener(this);
yourTextView.setCustomSelectionActionModeCallback(emptyActionMode);
}
#Override
public void onScrollChanged() {
// Anchor the popup while the user scrolls
if (popupWindow.isShowing()) {
final Point ploc = calculatePopupLocation();
popupWindow.update(ploc.x, ploc.y, DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
#Override
public void onTextSelected() {
final View popupContent = popupWindow.getContentView();
if (popupWindow.isShowing()) {
// Calculate the updated x/y pop coordinates
final Point ploc = calculatePopupLocation();
popupWindow.update(ploc.x, ploc.y, DEFAULT_WIDTH, DEFAULT_HEIGHT);
} else {
// Add the popup to the Window and position it relative to the selected text bounds
ViewUtils.onGlobalLayout(yourTextView, () -> {
popupWindow.showAtLocation(yourTextView, TOP, 0, 0);
// Wait for the popup content to be laid out
ViewUtils.onGlobalLayout(popupContent, () -> {
final Rect cframe = new Rect();
final int[] cloc = new int[2];
popupContent.getLocationOnScreen(cloc);
popupContent.getLocalVisibleRect(cbounds);
popupContent.getWindowVisibleDisplayFrame(cframe);
final int scrollY = ((View) yourTextView.getParent()).getScrollY();
final int[] tloc = new int[2];
yourTextView.getLocationInWindow(tloc);
final int startX = cloc[0] + cbounds.centerX();
final int startY = cloc[1] + cbounds.centerY() - (tloc[1] - cframe.top) - scrollY;
startLoc.set(startX, startY);
final Point ploc = calculatePopupLocation();
popupWindow.update(ploc.x, ploc.y, DEFAULT_WIDTH, DEFAULT_HEIGHT);
});
});
}
}
#Override
public void onTextUnselected() {
popupWindow.dismiss();
}
/** Used to calculate where we should position the {#link PopupWindow} */
private Point calculatePopupLocation() {
final ScrollView parent = (ScrollView) yourTextView.getParent();
// Calculate the selection start and end offset
final int selStart = yourTextView.getSelectionStart();
final int selEnd = yourTextView.getSelectionEnd();
final int min = Math.max(0, Math.min(selStart, selEnd));
final int max = Math.max(0, Math.max(selStart, selEnd));
// Calculate the selection bounds
final RectF selBounds = new RectF();
final Path selection = new Path();
yourTextView.getLayout().getSelectionPath(min, max, selection);
selection.computeBounds(selBounds, true /* this param is ignored */);
// Retrieve the center x/y of the popup content
final int cx = startLoc.x;
final int cy = startLoc.y;
// Calculate the top and bottom offset of the popup relative to the selection bounds
final int popupHeight = cbounds.height();
final int textPadding = yourTextView.getPaddingLeft();
final int topOffset = Math.round(selBounds.top - cy);
final int btmOffset = Math.round(selBounds.bottom - (cy - popupHeight));
// Calculate the x/y coordinates for the popup relative to the selection bounds
final int scrollY = parent.getScrollY();
final int x = Math.round(selBounds.centerX() + textPadding - cx);
final int y = Math.round(selBounds.top - scrollY < cy ? btmOffset : topOffset);
currLoc.set(x, y - scrollY);
return currLoc;
}
/** An {#link ActionMode.Callback} used to remove all action items from text selection */
static final class EmptyActionMode extends SimpleActionModeCallback {
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Return true to ensure the yourTextView is still selectable
return true;
}
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Remove all action items to provide an actionmode-less selection
menu.clear();
return true;
}
}
}
Results
With the action bar (link to video):
Without the action bar (link to video):
Bonus - animation
Because we know the starting location of the PopupWindow and the offset location as the selection changes, we can easily perform a linear interpolation between the two values to create a nice animation when we're moving things around.
public static float lerp(float a, float b, float v) {
return a + (b - a) * v;
}
private static final int DEFAULT_ANIM_DUR = 350;
private static final int DEFAULT_ANIM_DELAY = 500;
#Override
public void onTextSelected() {
final View popupContent = popupWindow.getContentView();
if (popupWindow.isShowing()) {
// Calculate the updated x/y pop coordinates
popupContent.getHandler().removeCallbacksAndMessages(null);
popupContent.postDelayed(() -> {
// The current x/y location of the popup
final int currx = currLoc.x;
final int curry = currLoc.y;
// Calculate the updated x/y pop coordinates
final Point ploc = calculatePopupLocation();
currLoc.set(ploc.x, ploc.y);
// Linear interpolate between the current and updated popup coordinates
final ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.addUpdateListener(animation -> {
final float v = (float) animation.getAnimatedValue();
final int x = Math.round(AnimUtils.lerp(currx, ploc.x, v));
final int y = Math.round(AnimUtils.lerp(curry, ploc.y, v));
popupWindow.update(x, y, DEFAULT_WIDTH, DEFAULT_HEIGHT);
});
anim.setDuration(DEFAULT_ANIM_DUR);
anim.start();
}, DEFAULT_ANIM_DELAY);
} else {
...
}
}
Results
With the action bar - animation (link to video)
Extra
I don't go into how to attach on click listeners to the popup actions and there are probably several ways to achieve this same effect with different calculations and implementations. But I will mention that if you wanted to retrieve the selected text and then do something with it, you'd just need to CharSequence.subSequence the min and max from the selected text.
Anyway, I hope this has been helpful! Let me know if you have any questions.
I would like to set a background drawable (or resource) on a TextView, not taking into account its compound drawable width (and paddings).
Getting the width of the compound (left one to be more precise), and its paddings should not be a problem, but the setting of the background on the width of the textview minus the width of the compound drawable (described above).
Should you have any advice on doing that, please let me know.
Here's the needed result:
PS. I thought about having a horizontal LinearLayout with an ImageView and TextView as its children, and having the background set on the textview only, but I am interested in having the same result with less Views (in this case, exactly one), if it is possible.
You could use a LayerDrawable which supports insets, for example:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="dimension" android:right="dimension">
<shape android:shape="rectangle">
<solid android:color="color" />
</shape>
</item>
</layer-list>
If you want to change your Drawable dynamically you're better off writing your own Drawable class. The following DividerDrawable for instance draws a line to a given padding onto a white background:
public class DividerDrawable extends Drawable {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float mDensity;
private int mPaddingLeft = 0;
public DividerDrawable(Context context) {
mPaint.setColor(Color.BLACK);
mDensity = context.getResources().getDisplayMetrics().density;
}
#Override
public void draw(Canvas canvas) {
int width = canvas.getWidth();
int height = canvas.getHeight();
canvas.drawColor(Color.WHITE);
canvas.drawRect(mPaddingLeft, height - mDensity, width, height, mPaint);
}
#Override
public void setAlpha(int alpha) {
}
#Override
public void setColorFilter(ColorFilter colorFilter) {
}
#Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
public void setPaddingLeft(int paddingLeft) {
if (mPaddingLeft != paddingLeft) {
mPaddingLeft = paddingLeft;
invalidateSelf();
}
}
}
To set the left padding based on your left CompoundDrawable you could do something like this:
private void setBackground(TextView textView, DividerDrawable background) {
Drawable drawableLeft = textView.getCompoundDrawables()[0];
int paddingLeft = drawableLeft != null ?
textView.getPaddingLeft() + drawableLeft.getIntrinsicWidth() + textView.getCompoundDrawablePadding() :
textView.getPaddingLeft();
background.setPaddingLeft(paddingLeft);
textView.setBackground(background);
}
To make good use of all this call it like so:
DividerDrawable dividerDrawable = new DividerDrawable(this);
TextView textView = (TextView) findViewById(R.id.text);
setBackground(textView, dividerDrawable);
I need to implement a ListView with triangular shaped items as shown in this image. The views that one adds to a ListView generally are rectangular in shape. Even in the documentation, a View is described as "occupies a rectangular area on the screen and is responsible for drawing and event handling".
How can I add non-rectangular shapes to the ListView and at the same time making sure that the click area is restricted to the shape, in this case a triangle.
Thank you!
My solution would use overlapping Views that are cropped to alternating triangles and only accept touch events within its triangle.
The issue is that the ListView does not really support overlapping item Views, therefore my example just loads all items at once into a ScrollView, which may be bad if you have more than, say, 30 items. Maybe this is doable with a RecyclerView but I haven't looked into that.
I have chosen to extend the FrameLayout to implement the triangle View logic, so you can use it as the root View of a list item and put anything you want in it:
public class TriangleFrameLayout extends FrameLayout {
// TODO: constructors
public enum Align { LEFT, RIGHT };
private Align alignment = Align.LEFT;
/**
* Specify whether it's a left or a right triangle.
*/
public void setTriangleAlignment(Align alignment) {
this.alignment = alignment;
}
#Override
public void draw(Canvas canvas) {
// crop drawing to the triangle shape
Path mask = new Path();
Point[] tria = getTriangle();
mask.moveTo(tria[0].x, tria[0].y);
mask.lineTo(tria[1].x, tria[1].y);
mask.lineTo(tria[2].x, tria[2].y);
mask.close();
canvas.save();
canvas.clipPath(mask);
super.draw(canvas);
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
// check if touch event is within the triangle shape
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
Point touch = new Point((int) event.getX(), (int) event.getY());
Point[] tria = getTriangle();
if (!isPointInsideTrigon(touch, tria[0], tria[1], tria[2])) {
// ignore touch event outside triangle
return false;
}
}
return super.onTouchEvent(event);
}
private boolean isPointInsideTrigon(Point s, Point a, Point b, Point c) {
// stolen from http://stackoverflow.com/a/9755252
int as_x = s.x - a.x;
int as_y = s.y - a.y;
boolean s_ab = (b.x - a.x) * as_y - (b.y - a.y) * as_x > 0;
if ((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0 == s_ab)
return false;
if ((c.x - b.x) * (s.y - b.y) - (c.y - b.y) * (s.x - b.x) > 0 != s_ab)
return false;
return true;
}
private Point[] getTriangle() {
// define the triangle shape of this View
boolean left = alignment == Align.LEFT;
Point a = new Point(left ? 0 : getWidth(), -1);
Point b = new Point(left ? 0 : getWidth(), getHeight() + 1);
Point c = new Point(left ? getWidth() : 0, getHeight() / 2);
return new Point[] { a, b, c };
}
}
An example item XML layout, with the TriangleFrameLayout as root, could look like this:
<?xml version="1.0" encoding="utf-8"?>
<your.package.TriangleFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/root_triangle"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginTop="-80dp"
android:clickable="true"
android:foreground="?attr/selectableItemBackground">
<TextView
android:id="#+id/item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="20dp"
android:textSize="30dp"
android:textStyle="bold"
android:textColor="#ffffff" />
</your.package.TriangleFrameLayout>
Here we have a fixed height of 160dp that you can change to whatever you want. The important thing is the negative top margin of half the height, -80dp in this case, that causes the items to overlap and the different triangles to match up.
Now we can inflate multiple such items and add it to a list, i.e. ScrollView. This shows an example layout for our Activity or Framgent:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
And the code to populate the list:
Here I created a dummy Adapter, analog to a ListView, that just enumerates our items from 0 to 15.
ListAdapter adapter = new BaseAdapter() {
#Override
public int getCount() { return 16; }
#Override
public Integer getItem(int position) { return position; }
#Override
public long getItemId(int position) { return position; }
#Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
view = getLayoutInflater().inflate(R.layout.item_tria, parent, false);
}
// determine whether it's a left or a right triangle
TriangleFrameLayout.Align align =
(position & 1) == 0 ? TriangleFrameLayout.Align.LEFT : TriangleFrameLayout.Align.RIGHT;
// setup the triangle
TriangleFrameLayout triangleFrameLayout = (TriangleFrameLayout) view.findViewById(R.id.root_triangle);
triangleFrameLayout.setTriangleAlignment(align);
triangleFrameLayout.setBackgroundColor(Color.argb(255, 0, (int) (Math.random() * 256), (int) (Math.random() * 256)));
// setup the example TextView
TextView textView = (TextView) view.findViewById(R.id.item_text);
textView.setText(getItem(position).toString());
textView.setGravity((position & 1) == 0 ? Gravity.LEFT : Gravity.RIGHT);
return view;
}
};
// populate the list
LinearLayout list = (LinearLayout) findViewById(R.id.list);
for (int i = 0; i < adapter.getCount(); ++i) {
final int position = i;
// generate the item View
View item = adapter.getView(position, null, list);
list.addView(item);
item.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Toast.makeText(v.getContext(), "#" + position, Toast.LENGTH_SHORT).show();
}
});
}
At the end we have a result that looks like this:
Two item of list for one row and make text clickable.
Design images for each row and two image for one item.
For each option make only text of both item clickable.
I do not think is is possible to create actual triangle shaped views and add them to list view. And what layout would you use in such a scenario?
One way to do it is using background images to create an illusion. Think of each segment separated by red lines as an item in list view. Therefore, you'll have to create the background images as required for each item in the list view and set them in the correct order.
Update: This is what i mean by consistent slicing of the background image in my comments below.
For those who are now using RecyclerView, an implementation of ItemDecoration can be set on RecyclerView which will:
Offset items : Override getItemOffsets() of decoration. Here one can move items so that they overlap.
Draw shapes : Override onDraw() to draw triangles as background.
I have an ActionBar icon (the main one on the left, not an action item) that I would like to animate.
I am setting my ActionBar's icon in my Activity like this:
getSupportActionBar().setIcon(icon)
where icon is a Drawable produced by a library that converts a custom XML view into a bitmap. This XML view is a RelativeLayout with a background image and a TextView on top.
Today, when I have to update the TextView I simply re-generate the icon and call setIcon again. Instead, I would like to get a hold of my TextView and apply some animation effect on it, like fade-out and then fade-in after updating it (maybe never having to call setIcon, just re-use the same one).
Not sure how to go about this. Can someone recommend an approach?
EDIT: trying this approach:
In MyActivity:
Drawable myDrawable = new MyDrawable();
supportActionBar.setIcon(myDrawable);
and:
public class MyDrawable extends Drawable {
private Paint paint;
private RectF rect;
public MyDrawable() {
this.paint = new Paint();
this.rect = new RectF();
}
#Override
public void draw(Canvas canvas) {
paint.setARGB(255, 0, 255, 0);
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.FILL);
rect.right = 20f;
rect.bottom = 20f;
canvas.drawRoundRect(rect, 0.5f, 0.5f, paint);
}
#Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
#Override
public void setColorFilter(ColorFilter cf) {
paint.setColorFilter(cf);
}
#Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
Nothing shows up. I verified that onDraw gets called. Something suspicious to me is that canvas has both height & width set to 1.
A proper approach for it would be to forget the XML layout and create a custom Drawable.
An instance of this custom drawable will be set to the icon on the ActionBar and call invalidateSelf() whenever necessary to redraw (due to animation, for example).
The drawable can hold reference to other drawables (e.g. BitmapDrawable to have something from the /res/ folder or a Color or Gradient drawable for a background shade) and call (for example) bgDraw.draw(canvas) during the onDraw callback.
It can also draw stuff directly on the canvas that is given to it during onDraw callback. With the canvas you can draw circle, lines, areas, path and text directly on it.
edit:
very simple animation example (didn't check the code, likely typos):
private long animationTime;
public void doAnimation(){
animationTime = System.currentTimeMilis() + 3000; // 3 seconds
invalidateSelf();
}
public void onDraw(Canvas canvas){
// do your drawing.
// You can use difference between
// currentTimeMilis and animationTime for status/position
...
// at the end
if(animationTime > System.currentTimeMilis())
invalidateSelf();
}