I'm currently working with graphics and paths, and I can successufully display whatever I want.
But instead of drawing a line directly on my SurfaceView, I'd like to draw it progressively in an animation.
What I've done so far is to create a Path and then to use PathMeasure to retrieve the coordinates progressively along the path. Here is basically what I've done so far
PathMeasure pm = new PathMeasure(myPath, false);
float position = 0;
float end = pm.getLength();
float[] coord = {0,0,0,0,0,0,0,0,0};
while (position < end){
Matrix m = new Matrix();
// put the current path position coordinates into the matrix
pm.getMatrix(position, m, PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
// put the matrix data into the coord array (coord[2] = x and coord[5] = y)
m.getValues(coord);
????
position += 1;
}
The question marks is where I'm stuck. I want to draw the path progressively and see it animated on the screen. I couldn't find much info about it on the internet, so any clue would be much appreciated if you have already come across the same situation. The final effect I want to create is like a pencil drawing progressively a text automatically.
Instead of creating a for loop, you can use the ObjectAnimator class to callback to one of your class's methods every time you'd like to draw a bit more of the path.
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;
import android.util.Log;
public class PathView extends View
{
Path path;
Paint paint;
float length;
public PathView(Context context)
{
super(context);
}
public PathView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public PathView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
public void init()
{
paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
path = new Path();
path.moveTo(50, 50);
path.lineTo(50, 500);
path.lineTo(200, 500);
path.lineTo(200, 300);
path.lineTo(350, 300);
// Measure the path
PathMeasure measure = new PathMeasure(path, false);
length = measure.getLength();
float[] intervals = new float[]{length, length};
ObjectAnimator animator = ObjectAnimator.ofFloat(PathView.this, "phase", 1.0f, 0.0f);
animator.setDuration(3000);
animator.start();
}
//is called by animtor object
public void setPhase(float phase)
{
Log.d("pathview","setPhase called with:" + String.valueOf(phase));
paint.setPathEffect(createPathEffect(length, phase, 0.0f));
invalidate();//will calll onDraw
}
private static PathEffect createPathEffect(float pathLength, float phase, float offset)
{
return new DashPathEffect(new float[] { pathLength, pathLength },
Math.max(phase * pathLength, offset));
}
#Override
public void onDraw(Canvas c)
{
super.onDraw(c);
c.drawPath(path, paint);
}
}
Then, just call init() to begin the animation, like this (or if you'd like it to start as soon as the view is inflated, put the init() call inside the constructors):
PathView path_view = (PathView) root_view.findViewById(R.id.path);
path_view.init();
Also see this question here, and this example, which I've based my code on.
I just have resolve this problem, here what I do:
private float[] mIntervals = { 0f, 0f };
private float drawSpeed = 2f;
private int currentPath = -1;
private PathMeasure mPathMeasure = new PathMeasure();
private ArrayList<Path> mListPath = new ArrayList<Path>(this.pathCount);
#Override
protected void onDraw(Canvas canvas) {
if (mIntervals[1] <= 0f && currentPath < (pathCount - 1)) {
// Set the current path to draw
// getPath(int num) a function to return a path.
Path newPath = this.getPath(mListPath.size());
this.mListPath.add(newPath);
this.mPathMeasure.setPath(newPath, false);
mIntervals[0] = 0;
mIntervals[1] = this.mPathMeasure.getLength();
}
if (mIntervals[1] > 0) {
// draw the previous path
int last = this.mListPath.size();
for (int i = 0; i < last; i++) {
canvas.drawPath(this.mListPath.get(i), mPaint);
}
// partially draw the last path
this.mPaint.setPathEffect(new DashPathEffect(mIntervals, 0f));
canvas.drawPath(this.mListPath.get(last), mPaint);
// Update the path effects values, to draw a little more
// on the path.
mIntervals[0] += drawSpeed;
mIntervals[1] -= drawSpeed;
super.invalidate();
} else {
// The drawing have been done, draw it entirely
for (int i = 0; i < this.mListPath.size(); i++) {
canvas.drawPath(this.mListPath.get(i), mPaint);
}
}
}
This example, is an adaptation of what I've done (to simplify the example). Hope you will understand it. Since I've just made this function working, It lacks of optimizations and things like that.
Hope it will help ;-)
here is an alternative solution that worked for me
package com.sample;
/**
* Created by Sumit
*/
public class PathView extends View {
Paint mPaint;
Path mPath;
int mStrokeColor;
float mStrokeWidth;
float mProgress = 0.0f;
float mLength = 0f;
float mTotal;
public PathView(Context context) {
this(context, null);
init();
}
public PathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public PathView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mStrokeColor = Color.RED;
mStrokeWidth = 8.0f;
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(mStrokeColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
setPath(new Path());
// setPath2(new Path());
}
public void setPath(Path p) {
mPath = p;
PathMeasure measure = new PathMeasure(mPath, false);
mPathLength = measure.getLength();
}
public void setPath(List<float[][]> list) {
Log.d("Path", "size " + list.size());
Path p = new Path();
p.moveTo(list.get(0)[0][0], list.get(1)[0][1]);
for (int i = 1; i < list.size(); i++) {
p.lineTo(list.get(i)[0][0], list.get(i)[0][1]);
//if (i > 100)
//p.moveTo(list.get(i)[0][0], list.get(i)[0][1]);
}
//p.setFillType(FillType.WINDING);
setPath(p);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mTotal = (mLength - mLength * mProgress);
PathEffect pathEffect = new DashPathEffect(new float[] { mLength, mLength }, mTotal);
Log.d("Path Tag", "length =" + mLength + ", totla=" + mTotal);
mPaint.setPathEffect(pathEffect);
canvas.save();
// canvas.translate(getPaddingLeft(), getPaddingTop());
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidth, measuredHeight;
if (widthMode == MeasureSpec.AT_MOST)
throw new IllegalStateException("Use MATCH_PARENT");
else
measuredWidth = widthSize;
if (heightMode == MeasureSpec.AT_MOST)
throw new IllegalStateException("Use MATCH_PARENT");
else
measuredHeight = heightSize;
setMeasuredDimension(measuredWidth, measuredHeight);
setPath();
}
void setPath() {
int cX = getWidth() / 2;
int cY = getHeight() / 2;
cY += 50;
cX -= 50;
List<float[][]> list = new ArrayList<float[][]>();
for (int i = 0; i < 50; i++) {
list.add(new float[][] { { cX--, cY++ } });
}
for (int i = 0; i < 100; i++) {
list.add(new float[][] { { cX--, cY-- } });
}
for (int i = 0; i < 100; i++) {
list.add(new float[][] { { cX++, cY-- } });
}
for (int i = 0; i < 200; i++) {
list.add(new float[][] { { cX++, cY++ } });
}
for (int i = 0; i < 100; i++) {
list.add(new float[][] { { cX++, cY-- } });
}
for (int i = 0; i < 100; i++) {
list.add(new float[][] { { cX--, cY-- } });
}
for (int i = 0; i < 100; i++) {
list.add(new float[][] { { cX--, cY++ } });
}
setPath(list);
}
}
and use
final PathView pathView = (PathView) findViewById(R.id.path_view);
pathView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
ObjectAnimator anim = ObjectAnimator.ofFloat(view, "percentage", 0.0f, 1.0f);
anim.setDuration(2000);
anim.setInterpolator(new LinearInterpolator());
anim.setRepeatCount(Animation.INFINITE);
anim.start();
}
});
You will have to add this view to the layout, setting height to 1 and width to match parent. The line will be animated from left to right. The later line will be placed over the first one.
public class AnimatorLineView extends RelativeLayout {
private View animatorLineView;
private View simpleLineView;
View animatorLine;
private int colorBeforeAnimation;
private int colorAfterAnimation;
private int colorForErrorLine;
public AnimatorLineView(Context context) {
super(context);
init();
startAnimation();
}
public AnimatorLineView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
initAttributes(context, attrs);
setColors();
startAnimation();
}
public AnimatorLineView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
initAttributes(context, attrs);
setColors();
startAnimation();
}
private void setColors() {
simpleLineView.setBackgroundColor(colorBeforeAnimation);
animatorLine.setBackgroundColor(colorAfterAnimation);
}
public void init() {
animatorLineView = inflate(getContext(), R.layout.ainimator_line_view, this);
animatorLine = findViewById(R.id.simple_line);
simpleLineView = findViewById(R.id.animator_line);
}
public void setColor(int color) {
animatorLine.setBackgroundColor(color);
}
public void startAnimation() {
animatorLine.setVisibility(View.VISIBLE);
Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.enter_animation_underline);
animatorLine.startAnimation(animation);
}
public void showErrorLine(){
animatorLine.setBackgroundColor(colorForErrorLine);
animatorLine.setVisibility(View.VISIBLE);
Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.enter_animation_underline);
animatorLine.startAnimation(animation);
}
public void hideErrorLine(){
animatorLine.setBackgroundColor(colorAfterAnimation);
animatorLine.setVisibility(View.VISIBLE);
Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.enter_animation_underline);
animatorLine.startAnimation(animation);
}
private void initAttributes(Context context, AttributeSet attributeSet) {
TypedArray attr = getTypedArray(context, attributeSet, R.styleable.ProgressButton);
if (attr == null) {
return;
}
try {
colorBeforeAnimation = attr.getColor(R.styleable.AnimatorLineView_al_color_after_animation,ContextCompat.getColor(getContext(), R.color.animation_line_text_color));
colorAfterAnimation = attr.getColor(R.styleable.ProgressButton_pb_text_color, ContextCompat.getColor(getContext(), R.color.black_color));
colorForErrorLine = attr.getColor(R.styleable.ProgressButton_pb_text_color, ContextCompat.getColor(getContext(), R.color.error_msgs_text_color));
} finally {
attr.recycle();
}
}
protected TypedArray getTypedArray(Context context, AttributeSet attributeSet, int[] attr) {
return context.obtainStyledAttributes(attributeSet, attr, 0, 0);
}
public void resetColor(){
animatorLine.setBackgroundColor(colorAfterAnimation);
animatorLine.setVisibility(View.GONE);
}
}
<animator_line_view>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<View
android:id="#+id/simple_line"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:background="#E0E0E0" />
<View
android:id="#+id/animator_line"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:background="#000000"
android:visibility="gone" />
</FrameLayout>
<enter_animation_underline>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:fromXDelta="-100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="#integer/animator_line_duration" />
</set>
---- styles------
<style name="animator_line">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="al_color_before_animation">#E0E0E0</item>
<item name="al_color_after_animation">#0000000</item>
<item name="al_error_line_color">#FF3352</item>
</style>
<declare-styleable name="AnimatorLineView">
<attr name="al_color_before_animation" format="color" />
<attr name="al_color_after_animation" format="color" />
<attr name="al_error_line_color" format="color" />
</declare-styleable>
-------- to be include in the xml
<com.careem.acma.widget.AnimatorLineView
android:id="#+id/animator_line"
style="#style/animator_line" />
Related
I know android.graphics is old, but i am having trouble doing a simple stuff.
I want to draw a line animation where one View points an arrow/line into another View
First Button-------------------------------->Second Button
I have tried creating a custom View class and overriding the onDraw(Canvas c) method and then using the drawLine(startX, startY, stopX, stopY, paint) method from the Canvas Object. But i don't know which coordinates to get in order to point one View to the other View
I don't want to create a static View in the XML layout with a slim height because the View can be added dynamically by the user, which i think drawing the line dynamically is the best way.
Please help me out. Thank you!
For drawing lines between views better if all of it lays on same parent layout. For the conditions of the question (Second Button is exactly to the right of First Button) you can use custom layout like that:
public class ArrowLayout extends RelativeLayout {
public static final String PROPERTY_X = "PROPERTY_X";
public static final String PROPERTY_Y = "PROPERTY_Y";
private final static double ARROW_ANGLE = Math.PI / 6;
private final static double ARROW_SIZE = 50;
private Paint mPaint;
private boolean mDrawArrow = false;
private Point mPointFrom = new Point(); // current (during animation) arrow start point
private Point mPointTo = new Point(); // current (during animation) arrow end point
public ArrowLayout(Context context) {
super(context);
init();
}
public ArrowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ArrowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ArrowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setWillNotDraw(false);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(5);
}
#Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.save();
if (mDrawArrow) {
drawArrowLines(mPointFrom, mPointTo, canvas);
}
canvas.restore();
}
private Point calcPointFrom(Rect fromViewBounds, Rect toViewBounds) {
Point pointFrom = new Point();
pointFrom.x = fromViewBounds.right;
pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
return pointFrom;
}
private Point calcPointTo(Rect fromViewBounds, Rect toViewBounds) {
Point pointTo = new Point();
pointTo.x = toViewBounds.left;
pointTo.y = toViewBounds.top + (toViewBounds.bottom - toViewBounds.top) / 2;
return pointTo;
}
private void drawArrowLines(Point pointFrom, Point pointTo, Canvas canvas) {
canvas.drawLine(pointFrom.x, pointFrom.y, pointTo.x, pointTo.y, mPaint);
double angle = Math.atan2(pointTo.y - pointFrom.y, pointTo.x - pointFrom.x);
int arrowX, arrowY;
arrowX = (int) (pointTo.x - ARROW_SIZE * Math.cos(angle + ARROW_ANGLE));
arrowY = (int) (pointTo.y - ARROW_SIZE * Math.sin(angle + ARROW_ANGLE));
canvas.drawLine(pointTo.x, pointTo.y, arrowX, arrowY, mPaint);
arrowX = (int) (pointTo.x - ARROW_SIZE * Math.cos(angle - ARROW_ANGLE));
arrowY = (int) (pointTo.y - ARROW_SIZE * Math.sin(angle - ARROW_ANGLE));
canvas.drawLine(pointTo.x, pointTo.y, arrowX, arrowY, mPaint);
}
public void animateArrows(int duration) {
mDrawArrow = true;
View fromView = getChildAt(0);
View toView = getChildAt(1);
// find from and to views bounds
Rect fromViewBounds = new Rect();
fromView.getDrawingRect(fromViewBounds);
offsetDescendantRectToMyCoords(fromView, fromViewBounds);
Rect toViewBounds = new Rect();
toView.getDrawingRect(toViewBounds);
offsetDescendantRectToMyCoords(toView, toViewBounds);
// calculate arrow sbegin and end points
Point pointFrom = calcPointFrom(fromViewBounds, toViewBounds);
Point pointTo = calcPointTo(fromViewBounds, toViewBounds);
ValueAnimator arrowAnimator = createArrowAnimator(pointFrom, pointTo, duration);
arrowAnimator.start();
}
private ValueAnimator createArrowAnimator(Point pointFrom, Point pointTo, int duration) {
final double angle = Math.atan2(pointTo.y - pointFrom.y, pointTo.x - pointFrom.x);
mPointFrom.x = pointFrom.x;
mPointFrom.y = pointFrom.y;
int firstX = (int) (pointFrom.x + ARROW_SIZE * Math.cos(angle));
int firstY = (int) (pointFrom.y + ARROW_SIZE * Math.sin(angle));
PropertyValuesHolder propertyX = PropertyValuesHolder.ofInt(PROPERTY_X, firstX, pointTo.x);
PropertyValuesHolder propertyY = PropertyValuesHolder.ofInt(PROPERTY_Y, firstY, pointTo.y);
ValueAnimator animator = new ValueAnimator();
animator.setValues(propertyX, propertyY);
animator.setDuration(duration);
// set other interpolator (if needed) here:
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPointTo.x = (int) valueAnimator.getAnimatedValue(PROPERTY_X);
mPointTo.y = (int) valueAnimator.getAnimatedValue(PROPERTY_Y);
invalidate();
}
});
return animator;
}
}
with .xml layout like:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<{YOUR_PACKAGE_NAME}.ArrowLayout
android:id="#+id/arrow_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/first_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:text="First Button"/>
<Button
android:id="#+id/second_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Second Button"/>
</{YOUR_PACKAGE_NAME}.ArrowLayout>
</RelativeLayout>
and MainActivity.java like:
public class MainActivity extends AppCompatActivity {
private ArrowLayout mArrowLayout;
private Button mFirstButton;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mArrowLayout = (ArrowLayout) findViewById(R.id.arrow_layout);
mFirstButton = (Button) findViewById(R.id.first_button);
mFirstButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mArrowLayout.animateArrows(1000);
}
});
}
}
you got something like that (on First Button click):
For other cases ( Second Button is exactly to the left (or above, or below) or more complex above-right/below-left etc. of First Button) you should modify part for calculating arrow begin and end points:
private Point calcPointFrom(Rect fromViewBounds, Rect toViewBounds) {
Point pointFrom = new Point();
// Second Button above
// ----------+----------
// | |
// Second Button tho the left + First Button + Second Button tho the right
// | |
// ----------+----------
// Second Button below
//
// + - is arrow start point position
if (toViewBounds to the right of fromViewBounds){
pointFrom.x = fromViewBounds.right;
pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
} else if (toViewBounds to the left of fromViewBounds) {
pointFrom.x = fromViewBounds.left;
pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
} else if () {
...
}
return pointFrom;
}
Use Path and Pathmeasure for Drawing Animated Line. I have Made and test it.
Make Custom View and pass view coordinates points array to it,
public class AnimatedLine extends View {
private final Paint mPaint;
public Canvas mCanvas;
AnimationListener animationListener;
Path path;
private static long animSpeedInMs = 2000;
private static final long animMsBetweenStrokes = 100;
private long animLastUpdate;
private boolean animRunning = true;
private int animCurrentCountour;
private float animCurrentPos;
private Path animPath;
private PathMeasure animPathMeasure;
float pathLength;
float distance = 0;
float[] pos;
float[] tan;
Matrix matrix;
Bitmap bm;
public AnimatedLine(Context context) {
this(context, null);
mCanvas = new Canvas();
}
public AnimatedLine(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(15);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setColor(context.getResources().getColor(R.color.materialcolorpicker__red));
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
}
bm = BitmapFactory.decodeResource(getResources(), R.drawable.hand1);
bm = Bitmap.createScaledBitmap(bm, 20,20, false);
distance = 0;
pos = new float[2];
tan = new float[2];
matrix = new Matrix();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCanvas = canvas;
if (path != null) {
if (animRunning) {
drawAnimation(mCanvas);
} else {
drawStatic(mCanvas);
}
}
}
/**
* draw Path With Animation
*
* #param time in milliseconds
*/
public void drawWithAnimation(ArrayList<PointF> points, long time,AnimationListener animationListener) {
animRunning = true;
animPathMeasure = null;
animSpeedInMs = time;
setPath(points);
setAnimationListener(animationListener);
invalidate();
}
public void setPath(ArrayList<PointF> points) {
if (points.size() < 2) {
throw new IllegalStateException("Pass atleast two points.");
}
path = new Path();
path.moveTo(points.get(0).x, points.get(0).y);
path.lineTo(points.get(1).x, points.get(1).y);
}
private void drawAnimation(Canvas canvas) {
if (animPathMeasure == null) {
// Start of animation. Set it up.
animationListener.onAnimationStarted();
animPathMeasure = new PathMeasure(path, false);
animPathMeasure.nextContour();
animPath = new Path();
animLastUpdate = System.currentTimeMillis();
animCurrentCountour = 0;
animCurrentPos = 0.0f;
pathLength = animPathMeasure.getLength();
} else {
// Get time since last frame
long now = System.currentTimeMillis();
long timeSinceLast = now - animLastUpdate;
if (animCurrentPos == 0.0f) {
timeSinceLast -= animMsBetweenStrokes;
}
if (timeSinceLast > 0) {
// Get next segment of path
float newPos = (float) (timeSinceLast) / (animSpeedInMs / pathLength) + animCurrentPos;
boolean moveTo = (animCurrentPos == 0.0f);
animPathMeasure.getSegment(animCurrentPos, newPos, animPath, moveTo);
animCurrentPos = newPos;
animLastUpdate = now;
//start draw bitmap along path
animPathMeasure.getPosTan(newPos, pos, tan);
matrix.reset();
matrix.postTranslate(pos[0], pos[1]);
canvas.drawBitmap(bm, matrix, null);
//end drawing bitmap
//take current position
animationListener.onAnimationUpdate(pos);
// If this stroke is done, move on to next
if (newPos > pathLength) {
animCurrentPos = 0.0f;
animCurrentCountour++;
boolean more = animPathMeasure.nextContour();
// Check if finished
if (!more) {
animationListener.onAnimationEnd();
animRunning = false;
}
}
}
// Draw path
canvas.drawPath(animPath, mPaint);
}
invalidate();
}
private void drawStatic(Canvas canvas) {
canvas.drawPath(path, mPaint);
canvas.drawBitmap(bm, matrix, null);
}
public void setAnimationListener(AnimationListener animationListener) {
this.animationListener = animationListener;
}
public interface AnimationListener {
void onAnimationStarted();
void onAnimationEnd();
void onAnimationUpdate(float[] pos);
}
}
I am trying to create a custom view that has a Circle and in it, I have to have sections in run time as shown in the image below. I tried a lot of stuff in onDraw method but got no luck. I even tried https://github.com/donvigo/CustomProgressControls . Basically, I want to give a number of sections and then in each section I can select colors as per my need.
I am looking for ProgressBar that should have gap/space as shown in the image; in between circles. Say if I have given 5 sections, 3 of which should be "full", it should color the first 3 in red, and the other 2 in green, for example.
To draw I am doing like:
private void initExternalCirclePainter() {
internalCirclePaint = new Paint();
internalCirclePaint.setAntiAlias(true);
internalCirclePaint.setStrokeWidth(internalStrokeWidth);
internalCirclePaint.setColor(color);
internalCirclePaint.setStyle(Paint.Style.STROKE);
internalCirclePaint.setPathEffect(new DashPathEffect(new float[]{dashWith, dashSpace}, dashSpace));
}
I might be a little late to the party, but I actually wrote a custom component that has 2 rings that look quite similar to what you're trying to achieve. You can just remove the outer ring easily. The image of what I got in the end:
Here's the class:
public class RoundedSectionProgressBar extends View {
// The amount of degrees that we wanna reserve for the divider between 2 sections
private static final float DIVIDER_ANGLE = 7;
public static final float DEGREES_IN_CIRCLE = 360;
public static final int PADDING = 18;
public static final int PADDING2 = 12;
protected final Paint paint = new Paint();
protected final Paint waitingPaint = new Paint();
protected final Paint backgroundPaint = new Paint();
private int totalSections = 5;
private int fullSections = 2;
private int waiting = 3; // The outer ring. You can omit this
private RectF rect = new RectF();
public RoundedSectionProgressBar(Context context) {
super(context);
init(context, null);
}
public RoundedSectionProgressBar(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RoundedSectionProgressBar(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// Can come from attrs if need be?
int strokeWidth = 3;
setupPaint(context, strokeWidth, paint, R.color.filled_color_inner_ring);
setupPaint(context, strokeWidth, waitingPaint, R.color.empty_color_inner_ring);
setupPaint(context, strokeWidth, backgroundPaint, R.color.filled_color_outer_ring);
}
private void setupPaint(Context context, int strokeWidth, Paint backgroundPaint, int colorRes) {
backgroundPaint.setStrokeCap(Paint.Cap.SQUARE);
backgroundPaint.setColor(context.getResources().getColor(colorRes));
backgroundPaint.setAntiAlias(true);
backgroundPaint.setStrokeWidth(strokeWidth);
backgroundPaint.setStyle(Paint.Style.STROKE);
}
public int getTotalSections() {
return totalSections;
}
public void setTotalSections(int totalSections) {
this.totalSections = totalSections;
invalidate();
}
public int getFullSections() {
return fullSections;
}
public void setNumberOfSections(int fullSections, int totalSections, int waiting) {
this.fullSections = fullSections;
this.totalSections = totalSections;
this.waiting = waiting;
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
rect.set(getLeft() + PADDING, getTop() + PADDING, getRight() - PADDING, getBottom() - PADDING);
float angleOfSection = (DEGREES_IN_CIRCLE / totalSections) - DIVIDER_ANGLE;
// Drawing the inner ring
for (int i = 0; i < totalSections; i++) {
// -90 because it doesn't start at the top, so rotate by -90
// divider_angle/2 especially in 2 sections, it's visibly rotated by Divider angle, so we split this between last and first
float startAngle = -90 + i * (angleOfSection + DIVIDER_ANGLE) + DIVIDER_ANGLE / 2;
if (i < fullSections) {
canvas.drawArc(rect, startAngle, angleOfSection, false, paint);
} else {
canvas.drawArc(rect, startAngle, angleOfSection, false, backgroundPaint);
}
}
// Drawing the outer ring
rect.set(getLeft() + PADDING2, getTop() + PADDING2, getRight() - PADDING2, getBottom() - PADDING2);
for (int i = 0; i < waiting; i++) {
float startAngle = -90 + i * (angleOfSection + DIVIDER_ANGLE) + DIVIDER_ANGLE / 2;
canvas.drawArc(rect, startAngle, angleOfSection, false, waitingPaint);
}
}
}
Notice that this code won't give you the outer ring's 'empty' slots, since we decided against them in the end. The inner circle will have both the empty and filled slots. The whole class can be reused, and it's responsible just for the 2 rings that are drawn, the 6/6, +3 and the red circle are parts of another view.
The most important piece of the code is the onDraw method. It contains the logic for drawing the arcs in the for loop, as well as the logic for calculating the angles and adding spaces between them. Everything is rotated by -90 degrees, because I needed it to start at the top, rather than on the right, as it is the 0-degree angle in Android. It's not that complex, and you can modify it to fit your needs better should you need to.
I find it easier to do math for drawArc(operating on angle values based on number of sections) rather than computing the arc length.
Here's a quick idea, with a lot of hard-coded properties, but you should be able to get the idea:
public class MyStrokeCircleView extends View {
private Paint mPaint;
private RectF mRect;
private int mPadding;
private int mSections;
private int mFullArcSliceLength;
private int mColorArcLineLength;
private int mArcSectionGap;
public MyStrokeCircleView(Context context) {
super(context);
init(null, 0);
}
public MyStrokeCircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public MyStrokeCircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle) {
mPaint = new Paint();
mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
mPaint.setColor(ContextCompat.getColor(getContext(), android.R.color.darker_gray));
mPadding = 5;
mRect = new RectF(mPadding, mPadding, mPadding, mPadding);
mSections = 4;
mFullArcSliceLength = 360 / mSections;
mArcSectionGap = mFullArcSliceLength / 10;
mColorArcLineLength = mFullArcSliceLength - 2 * mArcSectionGap;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mRect.right = getWidth() - mPadding;
mRect.bottom = getHeight() - mPadding;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mSections; i++) {
canvas.drawArc(mRect, i * mFullArcSliceLength + mArcSectionGap, mColorArcLineLength, false, mPaint);
}
}
}
I am looking to implement some sort of "canvas" where you can place X number of TextViews/Links at "random positions" (Positioned like in the image below). You would then be able to scroll this "canvas" view left or right continuously and the view will repeat/be circular (sort of like a HTML marquee except that you are doing the scrolling manually). In the most simplest of cases I am just looking to have horizontal scrolling - but an example of a more "complex case" is where you can do "sphere scrolling" - see the example below from Appy Geek. (For now I am just interested in the horizontal scrolling)
Example from Appy Geek:
Well this will get you started, I have implemented a simple tag cloud using both approaches (i.e. by extending View and ViewGroup) that keeps rotating. You can use this logic in your custom ViewGroup which positions its View's accordingly. After that add clickable TextViews inside that layout and handle touch events.
Final result (ofcourse its rotating, look closer):
Lot of things can be improved in the following code.
BY EXTENDING ViewGroup:
Put this in xml layout:
<com.vj.tagcloud.TagCloudLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
</com.vj.tagcloud.TagCloudLayout>
TagCloudLayout class:
import java.util.Random;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class TagCloudLayout extends ViewGroup {
final Random mRandom = new Random();
private float mRotateAngle;
private Handler mHandler = new Handler();
private float rotateAngleDegree;
public TagCloudLayout(Context context) {
super(context);
}
public TagCloudLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TagCloudLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final float radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2F;
float halfWidth = getMeasuredWidth() / 2F;
float halfHeight = getMeasuredHeight() / 2F;
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
float sinTheta = (float) Math.sin(lp.theta);
float x = (int) (radius * Math.cos(lp.fi + mRotateAngle)
* sinTheta);
if (child instanceof TextView) {
((TextView) child)
.setTextSize(15 * ((radius - x) / radius) + 10);
}
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// http://en.wikipedia.org/wiki/Spherical_coordinates
lp.x = (int) ((halfWidth + radius * Math.sin(lp.fi + mRotateAngle)
* sinTheta) - /* for balancing on x-axis */(child
.getMeasuredWidth() / 2F));
lp.y = (int) (halfHeight + radius * Math.cos(lp.theta)-/* for balancing on y-axis */(child
.getMeasuredHeight() / 2F));
}
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
rotateAngleDegree += 5;
mRotateAngle = (float) Math.toRadians(rotateAngleDegree);
requestLayout();
mHandler.postDelayed(this, 40);
}
}, 40);
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacksAndMessages(null);
}
#Override
public void addView(View child, int index,
android.view.ViewGroup.LayoutParams params) {
super.addView(child, index, params);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.fi = (float) Math.toRadians(mRandom.nextInt(360));
lp.theta = (float) Math.toRadians(mRandom.nextInt(360));
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
+ child.getMeasuredHeight());
}
}
#Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
#Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
#Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
#Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p.width, p.height);
}
public static class LayoutParams extends ViewGroup.LayoutParams {
int x;
int y;
float fi, theta;
public LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutParams(int w, int h) {
super(w, h);
}
}
}
BY EXTENDING View:
Put this in xml layout:
<com.vj.wordtap.TagCloud
android:layout_width="match_parent"
android:layout_height="match_parent" />
and this in java code:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import android.content.Context;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
public class TagCloud extends View {
private List<String> mItems = new ArrayList<String>();
private List<Angles> mAngles = new ArrayList<Angles>();
private Camera mCamera = new Camera();
private TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
private Handler mHandler = new Handler();
private float mRotateAngle;
private float rotateAngleDegree;
public static class Angles {
float fi, theta;
}
public TagCloud(Context context) {
super(context);
init();
}
public TagCloud(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TagCloud(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
List<String> items = new ArrayList<String>();
for (int i = 0; i < 10; i++) {
items.add("item:" + i);
}
setItems(items);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(canvas.getWidth() / 2F, canvas.getHeight() / 2F);
mTextPaint.setColor(Color.BLACK);
final float radius = 100;
mCamera.setLocation(0, 0, -100);
for (int i = 0; i < mItems.size(); i++) {
String item = mItems.get(i);
Angles xyz = mAngles.get(i);
mCamera.save();
canvas.save();
float sinTheta = (float) Math.sin(xyz.theta);
float x = (float) (radius * Math.cos(xyz.fi + mRotateAngle) * sinTheta);
float y = (float) (radius * Math.sin(xyz.fi + mRotateAngle) * sinTheta);
float z = (float) (radius * Math.cos(xyz.theta));
// mapping coordinates with Android's coordinates
// http://en.wikipedia.org/wiki/Spherical_coordinates
mCamera.translate(y, z, x);
mCamera.applyToCanvas(canvas);
// http://en.wikipedia.org/wiki/Spherical_coordinates
// set size based on x-Axis that is coming towards us
mTextPaint.setTextSize(20 * ((100 - x) / 100) + 10);
canvas.drawText(item, 0, 0, mTextPaint);
mCamera.restore();
canvas.restore();
}
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
rotateAngleDegree += 5;
mRotateAngle = (float) Math.toRadians(rotateAngleDegree);
invalidate();
mHandler.postDelayed(this, 40);
}
}, 40);
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacksAndMessages(null);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setItems(List<String> items) {
mItems = items;
final Random ran = new Random();
final List<Angles> xyzList = mAngles;
xyzList.clear();
for (int i = 0; i < items.size(); i++) {
Angles xyz = new Angles();
float fi = (float) Math.toRadians(ran.nextInt(360));
xyz.fi = fi;
float theta = (float) Math.toRadians(ran.nextInt(360));
xyz.theta = theta;
xyzList.add(xyz);
}
}
}
I want to draw graph like shown in the attached image.
I already tried by aChartEngine but it's not working successfully.
You could create a SurfaceView, in which you can draw to a Canvas in the onDraw() method. To draw your graph, you can use the Path class, and it's moveTo() and lineTo() methods. To change the appearance of the lines, use the Paint class. Then use the Canvases drawPath() method, which takes a Path, and a Paint object. I think it's a bit easier to start with, than OpenGl.
SurfaceView
Canvas
Path
Paint
Some tutorial
Update:
#Shakti Malik found a pretty good looking library, which looks easy to use: MPAndroidChart
How about trying OpenGL ES ?
you can create a GraphView which will extends GLSurfaceView
example code-
public class GraphView extends GLSurfaceView {
private Renderer renderer;
public GraphView(Context context) {
super(context);
renderer = new GraphRenderer();
setRenderer(renderer);
}
}
And your GraphRender
ublic class GraphRenderer implements Renderer {
public void onDrawFrame(GL10 gl) {
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 1.0f);
gl.glColor4f(1, 0, 0, .5f);
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
float ratio = (float) width / height;
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7);
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
private void drawGraph(GL10 gl) {
gl.glLineWidth(1.0f);
// put your code here ..
}
public static int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
}
You can try this way.
Example code of Canvas + Paint:
In your XML layout:
<com.y30.histogramdisplay.GraphView
android:id="#+id/histogram_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent" />
In the Activity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GraphView graphView = (GraphView)findViewById(R.id.histogram_view);
int graphArray[] = new int[256];
for(int i = 0; i < graphArray.length; ++i) {
graphArray[i] = i % 50;
}
graphView.setGraphArray(graphArray);
}
And the new View:
public class GraphView extends View {
int m_graphArray[] = null;
int m_maxY = 0;
Paint m_paint;
public GraphView(Context context) {
super(context);
init();
}
public GraphView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public GraphView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
m_paint = new Paint();
m_paint.setColor(Color.BLUE);
m_paint.setStrokeWidth(10);
}
public void setGraphArray(int Xi_graphArray[], int Xi_maxY)
{
m_graphArray = Xi_graphArray;
m_maxY = Xi_maxY;
}
public void setGraphArray(int Xi_graphArray[])
{
int maxY = 0;
for(int i = 0; i < Xi_graphArray.length; ++i)
{
if(Xi_graphArray[i] > maxY)
{
maxY = Xi_graphArray[i];
}
}
setGraphArray(Xi_graphArray, maxY);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(m_graphArray == null)
{
return;
}
int maxX = m_graphArray.length;
float factorX = getWidth() / (float)maxX;
float factorY = getHeight() / (float)m_maxY;
for(int i = 1; i < m_graphArray.length; ++i) {
int x0 = i - 1;
int y0 = m_graphArray[i-1];
int x1 = i;
int y1 = m_graphArray[i];
int sx = (int)(x0 * factorX);
int sy = getHeight() - (int)(y0* factorY);
int ex = (int)(x1*factorX);
int ey = getHeight() - (int)(y1* factorY);
canvas.drawLine(sx, sy, ex, ey, m_paint);
}
}
}
implementation 'com.jjoe64:graphview:4.2.1'
eduGrades = new String[5];
behGrades = new String[5];
eduGrades[0] = getString(R.string.fail);
eduGrades[1] = getString(R.string.pass);
eduGrades[2] = getString(R.string.good);
eduGrades[3] = getString(R.string.very_good);
eduGrades[4] = getString(R.string.excellent);
behGrades[0] = getString(R.string.baad);
behGrades[1] = getString(R.string.accepted);
behGrades[2] = getString(R.string.good);
behGrades[3] = getString(R.string.very_good);
behGrades[4] = getString(R.string.excellent);
DataPoint[] eduDp = new DataPoint[results.size()];
DataPoint[] behDp = new DataPoint[results.size()];
dates = new String[results.size()];
for (int i = 0; i < results.size(); i++) {
dates[i] = results.get(i).getDateOfNote();
eduDp[i] = new DataPoint(i, (double) results.get(i).getEducationEvaluationSign());
behDp[i] = new DataPoint(i, (double) results.get(i).getBehaviorEvaluationSign());
}
LineGraphSeries<DataPoint> eduSeries = new LineGraphSeries<>(eduDp);
educationalGraphView.addSeries(eduSeries);
eduSeries.setDrawBackground(true);
eduSeries.setColor(getResources().getColor(R.color.blue));
eduSeries.setBackgroundColor(getResources().getColor(R.color.blue));
StaticLabelsFormatter staticLabelsFormatter;
staticLabelsFormatter = new StaticLabelsFormatter(educationalGraphView);
staticLabelsFormatter.setVerticalLabels(eduGrades);
staticLabelsFormatter.setHorizontalLabels(dates);
educationalGraphView.getGridLabelRenderer().setHorizontalLabelsColor(getResources().getColor(R.color.colorPrimaryDark));
educationalGraphView.getGridLabelRenderer().setVerticalLabelsColor(getResources().getColor(R.color.colorPrimaryDark));
educationalGraphView.getGridLabelRenderer().setGridColor(getResources().getColor(R.color.white));
educationalGraphView.getGridLabelRenderer().setHorizontalLabelsAngle(145);
educationalGraphView.getGridLabelRenderer().setTextSize(23f);
educationalGraphView.getGridLabelRenderer().setLabelsSpace(20);
educationalGraphView.getGridLabelRenderer().setLabelFormatter(staticLabelsFormatter);
Here is the custom class of GradientTextView provided by #koush (source is there on (github)
Now I got the class but how to call this class anyone can help since I am new bee on android
As far as I understood it can use custom attribute to but nyways how to call this class from MainActivity to get run. I dont know how to send AttributeSet parameters. it's too confusing
package android.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.LinearGradient;
import android.graphics.Shader.TileMode;
import android.text.BoringLayout;
import android.util.AttributeSet;
import android.widget.TextView;
public class GradientTextView extends TextView {
public GradientTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
int mStartColor = 0;
int mEndColor = 0;
float mAngle;
String mText;
BoringLayout mLayout;
public GradientTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
int[] ids = new int[attrs.getAttributeCount()];
for (int i = 0; i < attrs.getAttributeCount(); i++) {
ids[i] = attrs.getAttributeNameResource(i);
}
TypedArray a = context.obtainStyledAttributes(attrs, ids, defStyle, 0);
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);
if (attrName == null)
continue;
if (attrName.equals("startColor")) {
mStartColor = a.getColor(i, -1);
}
else if (attrName.equals("endColor")) {
mEndColor = a.getColor(i, -1);
}
else if (attrName.equals("angle")) {
mAngle = a.getFloat(i, 0);
}
}
}
public static void setGradient(TextView tv, float angle, int startColor, int endColor) {
tv.measure(tv.getLayoutParams().width, tv.getLayoutParams().height);
LinearGradient gradient = getGradient(tv.getMeasuredWidth(), tv.getMeasuredHeight(), angle, startColor, endColor);
tv.getPaint().setShader(gradient);
}
static LinearGradient getGradient(int measuredWidth, int measuredHeight, float angle, int startColor, int endColor) {
// calculate a vector for this angle
double rad = Math.toRadians(angle);
double oa = Math.tan(rad);
double x;
double y;
if (oa == Double.POSITIVE_INFINITY) {
y = 1;
x = 0;
}
else if (oa == Double.NEGATIVE_INFINITY) {
y = -1;
x = 0;
}
else {
y = oa;
if (rad > Math.PI)
x = -1;
else
x = 1;
}
// using the vector, calculate the start and end points from the center of the box
int mx = measuredWidth;
int my = measuredHeight;
int cx = mx / 2;
int cy = my / 2;
double n;
if (x == 0) {
n = (double)cy / y;
}
else if (y == 0) {
n = (double)cx / x;
}
else {
n = (double)cy / y;
double n2 = (double)cx / x;
if (Math.abs(n2) < Math.abs(n))
n = n2;
}
int sx = (int)(cx - n * x);
int sy = (int)(cy - n * y);
int ex = (int)(cx + n * x);
int ey = (int)(cy + n * y);
return new LinearGradient(sx, sy, ex, ey, startColor, endColor, TileMode.CLAMP);
}
protected void onDraw(android.graphics.Canvas canvas) {
if (mGradient == null) {
mGradient = getGradient(getMeasuredWidth(), getMeasuredHeight(), mAngle, mStartColor, mEndColor);
getPaint().setShader(mGradient);
}
super.onDraw(canvas);
}
public int getStartColor() {
return mStartColor;
}
public void setStartColor(int startColor) {
mStartColor = startColor;
invalidate();
}
public int getEndColor() {
return mEndColor;
}
public void setEndColor(int endColor) {
mEndColor = endColor;
invalidate();
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mGradient = null;
}
LinearGradient mGradient;
}
This custom class are too confusing but they are used for tweaking like outline, gradients, and etc I am very keen to understand the flow
you can use this "View Element" by instanciating/using a view object:
TextView myView = new TextView (this);
myView.setText("Test");
GradientTextView.setGradient(myView, 90, Color.BLACK, Color.WHITE);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
Layoutparams.WRAP_CONTENT);
myLayout.addView(myView, params);
You can even use this object using it your layouts XML file.