Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): implement borderRadius corner support #11796

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2012-2017 by Axway, Inc. All Rights Reserved.
* Copyright (c) 2020 by Axway, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
package org.appcelerator.titanium.view;

import com.nineoldandroids.view.ViewHelper;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.TiDimension;
import org.appcelerator.titanium.util.TiConvert;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
Expand All @@ -19,9 +22,10 @@
import android.graphics.RectF;
import android.os.Build;
import android.view.View;
import android.widget.FrameLayout;
import android.view.ViewOutlineProvider;
import android.graphics.Outline;
import android.widget.FrameLayout;

import com.nineoldandroids.view.ViewHelper;

/**
* This class is a wrapper for Titanium Views with borders. Any view that specifies a border
Expand All @@ -33,7 +37,7 @@ public class TiBorderWrapperView extends FrameLayout

private int color = Color.TRANSPARENT;
private int backgroundColor = Color.TRANSPARENT;
private float radius = 0;
private float[] radius = { 0, 0, 0, 0, 0, 0, 0, 0 };
private float borderWidth = 0;
private int alpha = -1;
private Paint paint;
Expand Down Expand Up @@ -66,17 +70,26 @@ protected void onDraw(Canvas canvas)
}

Path outerPath = new Path();
if (radius > 0f) {
float innerRadius = radius - padding;
if (innerRadius > 0f) {
outerPath.addRoundRect(innerRect, innerRadius, innerRadius, Direction.CW);
if (hasRadius()) {
float[] innerRadius = new float[this.radius.length];
for (int i = 0; i < this.radius.length; i++) {
innerRadius[i] = this.radius[i] - padding;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Set specified border corners.
outerPath.addRoundRect(innerRect, innerRadius, Direction.CW);
} else {
outerPath.addRect(innerRect, Direction.CW);
outerPath.addRoundRect(innerRect, innerRadius[0], innerRadius[0], Direction.CW);
}
Path innerPath = new Path(outerPath);

// draw border
outerPath.addRoundRect(outerRect, radius, radius, Direction.CCW);
// Draw border.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Set specified border corners.
outerPath.addRoundRect(outerRect, this.radius, Direction.CW);
} else {
outerPath.addRoundRect(outerRect, this.radius[0], this.radius[0], Direction.CCW);
}
canvas.drawPath(outerPath, paint);

// TIMOB-16909: hack to fix anti-aliasing
Expand All @@ -101,7 +114,7 @@ protected void onDraw(Canvas canvas)
@Override
public void getOutline(View view, Outline outline)
{
outline.setRoundRect(bounds, radius);
outline.setRoundRect(bounds, radius[0]);
}
};
setOutlineProvider(viewOutlineProvider);
Expand All @@ -128,9 +141,92 @@ public void setBgColor(int color)
this.backgroundColor = color;
}

public void setRadius(float radius)
public boolean hasRadius()
{
for (float r : this.radius) {
if (r > 0f) {
return true;
}
}
return false;
}

public void setRadius(Object obj)
{
this.radius = radius;
if (obj instanceof Object[]) {
final Object[] cornerObjects = (Object[]) obj;
final float[] cornerPixels = new float[cornerObjects.length];

for (int i = 0; i < cornerObjects.length; i++) {
final Object corner = cornerObjects[i];
final TiDimension radiusDimension = TiConvert.toTiDimension(corner, TiDimension.TYPE_WIDTH);
if (radiusDimension != null) {
cornerPixels[i] = (float) radiusDimension.getPixels(this);
} else {
Log.w(TAG, "Invalid value specified for borderRadius[" + i + "].");
cornerPixels[i] = 0;
}
}

if (cornerPixels.length >= 4) {

// Top-Left, Top-Right, Bottom-Right, Bottom-Left
this.radius[0] = cornerPixels[0];
this.radius[1] = cornerPixels[0];
this.radius[2] = cornerPixels[1];
this.radius[3] = cornerPixels[1];
this.radius[4] = cornerPixels[2];
this.radius[5] = cornerPixels[2];
this.radius[6] = cornerPixels[3];
this.radius[7] = cornerPixels[3];

} else if (cornerPixels.length >= 2) {

// Top-Left, Bottom-Right and Top-Right, Bottom-Left
this.radius[0] = cornerPixels[0];
this.radius[1] = cornerPixels[0];
this.radius[2] = cornerPixels[0];
this.radius[3] = cornerPixels[0];
this.radius[4] = cornerPixels[1];
this.radius[5] = cornerPixels[1];
this.radius[6] = cornerPixels[1];
this.radius[7] = cornerPixels[1];

} else if (cornerPixels.length == 1) {

// Set all radius.
for (int i = 0; i < radius.length; i++) {
this.radius[i] = cornerPixels[0];
}

} else {
Log.w(TAG, "Could not set borderRadius, empty array.");
}

} else if (obj instanceof Object) {

// Support string formatting for multiple corners.
if (obj instanceof String) {
final String[] corners = ((String) obj).split("\\s");
if (corners != null && corners.length > 1) {
setRadius(corners);
return;
}
}

final TiDimension radiusDimension = TiConvert.toTiDimension(obj, TiDimension.TYPE_WIDTH);
float pixels = 0;

if (radiusDimension != null) {
pixels = (float) radiusDimension.getPixels(this);
} else {
Log.w(TAG, "Invalid value specified for borderRadius.");
}

for (int i = 0; i < radius.length; i++) {
this.radius[i] = pixels;
}
}
}

public void setBorderWidth(float borderWidth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,7 @@ protected boolean hasBorder(KrollDict d)
|| (d.containsKeyAndNotNull(TiC.PROPERTY_BORDER_WIDTH)
&& TiConvert.toTiDimension(d.getString(TiC.PROPERTY_BORDER_WIDTH), TiDimension.TYPE_WIDTH).getValue()
> 0f)
|| (d.containsKeyAndNotNull(TiC.PROPERTY_BORDER_RADIUS)
&& TiConvert.toTiDimension(d.getString(TiC.PROPERTY_BORDER_RADIUS), TiDimension.TYPE_WIDTH).getValue()
> 0f);
|| (d.containsKeyAndNotNull(TiC.PROPERTY_BORDER_RADIUS));
}

private boolean hasColorState(KrollDict d)
Expand Down Expand Up @@ -1460,17 +1458,10 @@ private void initializeBorder(KrollDict d, Integer bgColor)
}

if (d.containsKey(TiC.PROPERTY_BORDER_RADIUS)) {
final float FLOAT_EPSILON = Math.ulp(1.0f);
float radius = 0;
TiDimension radiusDim =
TiConvert.toTiDimension(d.get(TiC.PROPERTY_BORDER_RADIUS), TiDimension.TYPE_WIDTH);
if (radiusDim != null) {
radius = (float) radiusDim.getPixels(getNativeView());
}
if ((radius >= FLOAT_EPSILON) && (d.containsKey(TiC.PROPERTY_OPACITY) && LOWER_THAN_MARSHMALLOW)) {
if (d.containsKey(TiC.PROPERTY_OPACITY) && LOWER_THAN_MARSHMALLOW) {
disableHWAcceleration();
}
borderView.setRadius(radius);
borderView.setRadius(d.get(TiC.PROPERTY_BORDER_RADIUS));
}

if (bgColor != null) {
Expand Down Expand Up @@ -1515,16 +1506,10 @@ private void handleBorderProperty(String property, Object value)
borderView.setBorderWidth(1);
}
} else if (TiC.PROPERTY_BORDER_RADIUS.equals(property)) {
final float FLOAT_EPSILON = Math.ulp(1.0f);
float radius = 0;
TiDimension radiusDim = TiConvert.toTiDimension(value, TiDimension.TYPE_WIDTH);
if (radiusDim != null) {
radius = (float) radiusDim.getPixels(getNativeView());
}
if ((radius > FLOAT_EPSILON) && (proxy.hasProperty(TiC.PROPERTY_OPACITY) && LOWER_THAN_MARSHMALLOW)) {
if (proxy.hasProperty(TiC.PROPERTY_OPACITY) && LOWER_THAN_MARSHMALLOW) {
disableHWAcceleration();
}
borderView.setRadius(radius);
borderView.setRadius(value);
} else if (TiC.PROPERTY_BORDER_WIDTH.equals(property)) {
float width = 0;
TiDimension bwidth = TiConvert.toTiDimension(value, TiDimension.TYPE_WIDTH);
Expand Down
5 changes: 4 additions & 1 deletion apidoc/Titanium/UI/View.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1332,8 +1332,11 @@ properties:
- name: borderRadius
summary: Radius for the rounded corners of the view's border.
description: Each corner is rounded using an arc of a circle.
type: Number
Values for each corner can be specified. For example, '20px 20px' will set both left and right corners to `20px`.
Specifying '20px 20px 20px 20px' will set top-left, top-right, bottom-right and bottom-left corners in that order.
type: [Number, String, Array<Number>, Array<String>]
default: 0
osver: {android: {min: "5.0"}}

- name: borderWidth
summary: Border width of the view.
Expand Down
98 changes: 98 additions & 0 deletions tests/Resources/ti.ui.view.addontest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Appcelerator Titanium Mobile
* Copyright (c) 2015-Present by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
/* eslint-env mocha */
/* eslint no-unused-expressions: "off" */
'use strict';
const should = require('./utilities/assertions');

describe('Titanium.UI.View', function () {
let rootWindow;
let win;

this.slow(2000);
this.timeout(10000);

before(function (finish) {
rootWindow = Ti.UI.createWindow();
rootWindow.addEventListener('open', () => finish());
rootWindow.open();
});

after(function (finish) {
rootWindow.addEventListener('close', () => finish());
rootWindow.close();
});

afterEach(function (done) {
if (win) {
// If `win` is already closed, we're done.
let t = setTimeout(function () {
if (win) {
win = null;
done();
}
}, 3000);

win.addEventListener('close', function listener () {
clearTimeout(t);

if (win) {
win.removeEventListener('close', listener);
}
win = null;
done();
});
win.close();
} else {
win = null;
done();
}
});

it.android('borderRadius corners (string)', finish => {
win = Ti.UI.createWindow({ backgroundColor: 'blue' });
const view = Ti.UI.createView({
width: 100,
height: 100,
borderRadius: '20px 20 20dp 20',
});

win.addEventListener('focus', () => {
try {
should(view.borderRadius).be.a.String();
} catch (err) {
return finish(err);
}
finish();
});

win.add(view);
win.open();
});

it.android('borderRadius corners (array)', finish => {
win = Ti.UI.createWindow({ backgroundColor: 'blue' });
const view = Ti.UI.createView({
width: 100,
height: 100,
borderRadius: [ '20px', 20, '20dp', '20' ],
});

win.addEventListener('focus', () => {
try {
should(view.borderRadius).be.an.Array();
should(view.borderRadius.length).eql(4);
} catch (err) {
return finish(err);
}
finish();
});

win.add(view);
win.open();
});
});