Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions apps/automated/src/ui/slider/slider-gradient-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as TKUnit from '../../tk-unit';
import * as helper from '../../ui-helper';
import * as sliderTestsNative from './slider-tests-native';
import { Observable, Color } from '@nativescript/core';
import { LinearGradient } from '@nativescript/core/ui/styling/linear-gradient';
import { Slider } from '@nativescript/core/ui/slider';

export function test_linear_gradient_layer_structure() {
const slider = new Slider();

function testAction() {
// Create a test gradient
const gradient = new LinearGradient();
gradient.angle = 90;
gradient.colorStops = [
{ color: new Color('#FF0000'), offset: { unit: '%', value: 0 } },
{ color: new Color('#00FF00'), offset: { unit: '%', value: 50 } },
{ color: new Color('#0000FF'), offset: { unit: '%', value: 100 } },
];

// Set the gradient
slider.minTrackGradient = gradient;

// Get native drawables
const nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);

if (__ANDROID__) {
// Test Android layer structure
TKUnit.assertTrue(nativeProgress instanceof android.graphics.drawable.LayerDrawable, 'Progress drawable should be a LayerDrawable');

const layerDrawable = nativeProgress as android.graphics.drawable.LayerDrawable;
TKUnit.assertEqual(layerDrawable.getNumberOfLayers(), 3, 'LayerDrawable should have 3 layers');

// Test that our gradient layer is in the correct position (progress layer)
const progressLayer = layerDrawable.getDrawable(layerDrawable.findIndexByLayerId(android.R.id.progress));
TKUnit.assertTrue(progressLayer instanceof org.nativescript.widgets.LinearGradientDefinition || progressLayer instanceof android.graphics.drawable.ShapeDrawable, 'Progress layer should be a gradient drawable');
} else if (__IOS__) {
// Test iOS layer structure
TKUnit.assertNotNull(nativeProgress, 'Progress image should not be null');
TKUnit.assertTrue(nativeProgress instanceof UIImage, 'Progress drawable should be a UIImage');
}
}

helper.buildUIAndRunTest(slider, testAction);
}

export function test_gradient_angle_mapping() {
const slider = new Slider();

function testAction() {
const angles = [0, 90, 180, 270];

angles.forEach((angle) => {
const gradient = new LinearGradient();
gradient.angle = angle;
gradient.colorStops = [{ color: new Color('#FF0000') }, { color: new Color('#0000FF') }];

slider.minTrackGradient = gradient;
const nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);

if (__ANDROID__) {
const layerDrawable = nativeProgress as android.graphics.drawable.LayerDrawable;
const progressLayer = layerDrawable.getDrawable(layerDrawable.findIndexByLayerId(android.R.id.progress));

// Verify the angle is correctly mapped to the native gradient
if (progressLayer instanceof org.nativescript.widgets.LinearGradientDefinition) {
const expectedStartX = angle === 180 ? 1 : angle === 0 ? 0 : 0.5;
const expectedStartY = angle === 270 ? 1 : angle === 90 ? 0 : 0.5;
const expectedEndX = angle === 0 ? 1 : angle === 180 ? 0 : 0.5;
const expectedEndY = angle === 90 ? 1 : angle === 270 ? 0 : 0.5;

// Validate the coordinates are within acceptable range
const startXDiff = Math.abs(progressLayer.getStartX() - expectedStartX);
const startYDiff = Math.abs(progressLayer.getStartY() - expectedStartY);
const endXDiff = Math.abs(progressLayer.getEndX() - expectedEndX);
const endYDiff = Math.abs(progressLayer.getEndY() - expectedEndY);

TKUnit.assertTrue(startXDiff <= 0.1, 'Start X coordinate should be approximately ' + expectedStartX);
TKUnit.assertTrue(startYDiff <= 0.1, 'Start Y coordinate should be approximately ' + expectedStartY);
TKUnit.assertTrue(endXDiff <= 0.1, 'End X coordinate should be approximately ' + expectedEndX);
TKUnit.assertTrue(endYDiff <= 0.1, 'End Y coordinate should be approximately ' + expectedEndY);
}
}
});
}

helper.buildUIAndRunTest(slider, testAction);
}

export function test_gradient_color_stops() {
const slider = new Slider();

function testAction() {
const gradient = new LinearGradient();
gradient.angle = 0;
gradient.colorStops = [
{ color: new Color('#FF0000'), offset: { unit: '%', value: 0 } },
{ color: new Color('#00FF00'), offset: { unit: '%', value: 50 } },
{ color: new Color('#0000FF'), offset: { unit: '%', value: 100 } },
];

slider.minTrackGradient = gradient;
const nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);

if (__ANDROID__) {
const layerDrawable = nativeProgress as android.graphics.drawable.LayerDrawable;
const progressLayer = layerDrawable.getDrawable(layerDrawable.findIndexByLayerId(android.R.id.progress));

if (progressLayer instanceof org.nativescript.widgets.LinearGradientDefinition) {
const colors = progressLayer.getColors();
const stops = progressLayer.getStops();

// Test color count
TKUnit.assertEqual(colors.length, 3, 'Should have 3 colors in the gradient');

// Test color values
TKUnit.assertEqual(colors[0], new Color('#FF0000').android, 'First color should be red');
TKUnit.assertEqual(colors[1], new Color('#00FF00').android, 'Second color should be green');
TKUnit.assertEqual(colors[2], new Color('#0000FF').android, 'Third color should be blue');

// Test stops
TKUnit.assertEqual(stops[0], 0, 'First stop should be at 0');
TKUnit.assertEqual(stops[1], 0.5, 'Second stop should be at 0.5');
TKUnit.assertEqual(stops[2], 1, 'Third stop should be at 1');
}
} else if (__IOS__) {
// iOS-specific color stop tests
TKUnit.assertNotNull(nativeProgress, 'Progress image should not be null');
}
}

helper.buildUIAndRunTest(slider, testAction);
}

export function test_gradient_updates() {
const slider = new Slider();

function testAction() {
// Initial gradient
const gradient1 = new LinearGradient();
gradient1.angle = 0;
gradient1.colorStops = [{ color: new Color('#FF0000') }, { color: new Color('#0000FF') }];

slider.minTrackGradient = gradient1;
let nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);
TKUnit.assertNotNull(nativeProgress, 'Initial gradient should be applied');

// Update gradient
const gradient2 = new LinearGradient();
gradient2.angle = 90;
gradient2.colorStops = [{ color: new Color('#00FF00') }, { color: new Color('#FF00FF') }];

slider.minTrackGradient = gradient2;
nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);
TKUnit.assertNotNull(nativeProgress, 'Updated gradient should be applied');

// Remove gradient
slider.minTrackGradient = null;
nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);
if (__ANDROID__) {
TKUnit.assertTrue(nativeProgress instanceof android.graphics.drawable.LayerDrawable, 'Should revert to default LayerDrawable');
}
}

helper.buildUIAndRunTest(slider, testAction);
}
17 changes: 17 additions & 0 deletions apps/automated/src/ui/slider/slider-tests-native.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Slider } from '@nativescript/core/ui/slider';

export function getNativeMinTrackImage(slider: Slider) {
if (!slider || !slider.android) {
return null;
}
try {
const seekBar = slider.android as android.widget.SeekBar;
return seekBar.getProgressDrawable();
} catch (e) {
return null;
}
}

export function getNativeProgressDrawable(slider: Slider) {
return getNativeMinTrackImage(slider);
}
4 changes: 4 additions & 0 deletions apps/automated/src/ui/slider/slider-tests-native.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Slider } from '@nativescript/core/ui/slider';

export function getNativeMinTrackImage(slider: Slider): any;
export function getNativeProgressDrawable(slider: Slider): any;
17 changes: 17 additions & 0 deletions apps/automated/src/ui/slider/slider-tests-native.ios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Slider } from '@nativescript/core/ui/slider';

export function getNativeMinTrackImage(slider: Slider) {
if (!slider || !slider.ios) {
return null;
}
try {
return slider.ios.minimumTrackImageForState(UIControlState.Normal);
} catch (e) {
return null;
}
}

export function getNativeProgressDrawable(slider: Slider) {
// On iOS the equivalent for progress drawable is minimum/maximum track images; return minimum track image
return getNativeMinTrackImage(slider);
}
21 changes: 21 additions & 0 deletions apps/automated/src/ui/slider/slider-tests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as TKUnit from '../../tk-unit';
import * as helper from '../../ui-helper';
import * as sliderTestsNative from './slider-tests-native';
import { BindingOptions, View, Page, Observable, EventData, PropertyChangeData, Color } from '@nativescript/core';
import { LinearGradient } from '@nativescript/core/ui/styling/linear-gradient';

// >> article-require-slider
import { Slider } from '@nativescript/core/ui/slider';
Expand Down Expand Up @@ -527,3 +529,22 @@ function setNativeValue(slider: Slider, value: number) {
slider.ios.sendActionsForControlEvents(UIControlEvents.ValueChanged);
}
}

export function test_set_gradients_native() {
const slider = new Slider();
function testAction(views: Array<View>) {
const gradient = new LinearGradient();
gradient.angle = 0;
gradient.colorStops = [{ color: new Color('#ff0000') }, { color: new Color('#00ff00') }];

slider.minTrackGradient = gradient;
slider.maxTrackGradient = gradient;

const nativeMin = sliderTestsNative.getNativeMinTrackImage(slider);
const nativeProgress = sliderTestsNative.getNativeProgressDrawable(slider);

TKUnit.assert(nativeMin !== null || nativeProgress !== null, 'Native gradient drawable or image should be applied on either platform');
}

helper.buildUIAndRunTest(new Slider(), testAction);
}
13 changes: 13 additions & 0 deletions packages/core/__tests__/slider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { minTrackGradientProperty, maxTrackGradientProperty } from '../ui/slider/slider-common';

describe('Slider gradient properties (unit)', () => {
test('minTrackGradientProperty is exported', () => {
expect(minTrackGradientProperty).toBeDefined();
expect(minTrackGradientProperty.name).toBe('minTrackGradient');
});

test('maxTrackGradientProperty is exported', () => {
expect(maxTrackGradientProperty).toBeDefined();
expect(maxTrackGradientProperty.name).toBe('maxTrackGradient');
});
});
86 changes: 86 additions & 0 deletions packages/core/ui/slider/gradient-drawable.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { LinearGradient } from '../styling/linear-gradient';

declare module 'globals' {
export const global: any;
}
declare const global: any;

export function fromGradient(gradient: LinearGradient): org.nativescript.widgets.LinearGradientDefinition {
const colors = Array.create('int', gradient.colorStops.length);
const stops = Array.create('float', gradient.colorStops.length);
let hasStops = false;
gradient.colorStops.forEach((stop, index) => {
colors[index] = stop.color.android;
if (stop.offset) {
stops[index] = stop.offset.value / 100; // Convert percentage to decimal
hasStops = true;
}
});

const alpha = gradient.angle / (Math.PI * 2);
const startX = Math.pow(Math.sin(Math.PI * (alpha + 0.75)), 2);
const startY = Math.pow(Math.sin(Math.PI * (alpha + 0.5)), 2);
const endX = Math.pow(Math.sin(Math.PI * (alpha + 0.25)), 2);
const endY = Math.pow(Math.sin(Math.PI * alpha), 2);

return new org.nativescript.widgets.LinearGradientDefinition(startX, startY, endX, endY, colors, hasStops ? stops : null);
}

@NativeClass()
class ShaderDrawable extends android.graphics.drawable.ShapeDrawable {
private gradient: org.nativescript.widgets.LinearGradientDefinition;
private paint: android.graphics.Paint;

constructor(gradient: org.nativescript.widgets.LinearGradientDefinition) {
super(new android.graphics.drawable.shapes.RectShape());
this.gradient = gradient;
this.paint = this.getPaint();
return global.__native(this);
}

public onBoundsChange(bounds: android.graphics.Rect): void {
super.onBoundsChange(bounds);
this.updateShader(bounds);
}

private updateShader(bounds: android.graphics.Rect): void {
const width = bounds.width();
const height = bounds.height();

if (width <= 0 || height <= 0) {
return;
}

const shader = new android.graphics.LinearGradient(this.gradient.getStartX() * width, this.gradient.getStartY() * height, this.gradient.getEndX() * width, this.gradient.getEndY() * height, this.gradient.getColors(), this.gradient.getStops(), android.graphics.Shader.TileMode.CLAMP);

this.paint.setShader(shader);
this.invalidateSelf();
}
}

@NativeClass()
export class GradientDrawable extends android.graphics.drawable.LayerDrawable {
constructor(gradient: LinearGradient, defaultDrawable: android.graphics.drawable.LayerDrawable) {
const drawableCount = defaultDrawable.getNumberOfLayers();
const drawables = Array.create('android.graphics.drawable.Drawable', drawableCount);
const shaderDrawable = new ShaderDrawable(fromGradient(gradient));

for (let i = 0; i < drawableCount; i++) {
const id = defaultDrawable.getId(i);
if (id === android.R.id.progress) {
drawables[i] = shaderDrawable;
} else {
drawables[i] = defaultDrawable.getDrawable(i);
}
}

super(drawables);

// Copy layer IDs from original drawable
for (let i = 0; i < drawableCount; i++) {
this.setId(i, defaultDrawable.getId(i));
}

return global.__native(this);
}
}
33 changes: 31 additions & 2 deletions packages/core/ui/slider/index.android.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Background } from '../styling/background';
import { SliderBase, valueProperty, minValueProperty, maxValueProperty } from './slider-common';
import { SliderBase, valueProperty, minValueProperty, maxValueProperty, minTrackGradientProperty, maxTrackGradientProperty } from './index.shared';
import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties';
import { Color } from '../../color';
import { AndroidHelper } from '../core/view';
import { LinearGradient } from '../styling/linear-gradient';
import { GradientDrawable } from './gradient-drawable.android';

export * from './slider-common';
export * from './index.shared';

interface OwnerSeekBar extends android.widget.SeekBar {
owner: Slider;
Expand Down Expand Up @@ -61,6 +63,8 @@ export class Slider extends SliderBase {
return new SeekBar(this._context);
}

private _defaultProgressDrawable: android.graphics.drawable.Drawable;

public initNativeView(): void {
super.initNativeView();
const nativeView = this.nativeViewProtected;
Expand Down Expand Up @@ -144,4 +148,29 @@ export class Slider extends SliderBase {
[backgroundInternalProperty.setNative](value: Background) {
//
}

[minTrackGradientProperty.setNative](value: LinearGradient | null) {
const nativeView = this.nativeViewProtected;
if (!nativeView) {
return;
}
if (!this._defaultProgressDrawable) {
this._defaultProgressDrawable = nativeView.getProgressDrawable();
}

if (!value) {
// restore original drawable
nativeView.setProgressDrawable(this._defaultProgressDrawable);
return;
}

// Create a new drawable with shader-based gradient
const drawable = new GradientDrawable(value, this._defaultProgressDrawable);
nativeView.setProgressDrawable(drawable);
}

[maxTrackGradientProperty.setNative](value: LinearGradient | null) {
// For now apply same drawable as min track as SeekBar uses a single progress drawable.
this[minTrackGradientProperty.setNative](value as any);
}
}
Loading