SwiftUI List styles - watchkit

I'm writing an Apple Watch app and I want to create a list just like this one in the Settings menu where the rows in the middle of the list are rectangles whereas the top and bottom are rounded rectangles. Is this a type of List style?

It is possible to do, though it requires using GeometryReader to handle drawing the rounded corners.
There is a great post by swiftui-lab.com that explains how to make the certain corners of a view rounded. Here is the code.
struct RoundedCorners: View {
var color: Color = .blue
var tl: CGFloat = 0.0
var tr: CGFloat = 0.0
var bl: CGFloat = 0.0
var br: CGFloat = 0.0
var body: some View {
GeometryReader { geometry in
Path { path in
let w = geometry.size.width
let h = geometry.size.height
// Make sure we do not exceed the size of the rectangle
let tr = min(min(self.tr, h/2), w/2)
let tl = min(min(self.tl, h/2), w/2)
let bl = min(min(self.bl, h/2), w/2)
let br = min(min(self.br, h/2), w/2)
path.move(to: CGPoint(x: w / 2.0, y: 0))
path.addLine(to: CGPoint(x: w - tr, y: 0))
path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
path.addLine(to: CGPoint(x: w, y: h - br))
path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
path.addLine(to: CGPoint(x: bl, y: h))
path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: tl))
path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
}
.fill(self.color)
}
}
}
We can then make a small example. Taking an array of items we can create a List. We will need the indices of the items so that we can tell which item is first (index of 0) and last (index of count - 1). The .listRowBackground modifier allows us to set the background of our rows so we will use that to set the view on it.
We create a helper function createRoundedCorners which will takes the index of the row and the number of rows that exist. This returns a RoundedCorrners view that the top left and right corners of the first row to be rounded and similarly the bottom left and right corners of the end row.
struct ContentView: View {
let items = ["Within 2 Minutes of Last Use",
"Within 1 Hour of Last Use",
"Always"]
var body: some View {
List {
ForEach(0..<items.count) { index in
Text(self.items[index]).font(.system(size: 24))
.listRowBackground(self.createRoundedCorners(at: index, count: self.items.count))
}
}
}
/// This function creates rounded corners for the first and last items in an array
func createRoundedCorners(at index: Int, count: Int) -> RoundedCorners {
switch index {
case 0:
return RoundedCorners(color: .blue, tl: 15, tr: 15, bl: 0, br: 0)
case (count - 1):
return RoundedCorners(color: .blue, tl: 0, tr: 0, bl: 15, br: 15)
default:
return RoundedCorners(color: .blue, tl: 0, tr: 0, bl: 0, br: 0)
}
}
}
This gives the following results
List at the top:
List at the bottom:

No, in SwiftUI Lists are very limited for configurations.
I recommend you to try making your own custom List-like view using ScrollView.

Related

Rendering imageData to new canvas

I'm following a tutorial by George Francis in the tutorial after some initial examples he shows how to use image data to create random layouts.
I'm trying to work out how to get the image data from a canvas created using paper.js, as I need to get the rgb values from each individual pixel on the canvas
Link to codepen
Unknowns:
Do I need to use the rasterize() method on the shape I've created?
Currently I am attempting the following:
// create a white rectangle the size of the view (not sure I need this but doing it so that there are both white and black pixels)
const bg = new paper.Path.Rectangle({
position: [0,0],
size: view.viewSize.multiply(2),
fillColor: 'white'
})
// create a black rectangle smaller than the view size
const shape = new paper.Path.RegularPolygon({
radius: view.viewSize.width * 0.4,
fillColor: 'black',
strokeColor: 'black',
sides: 4,
position: view.center
})
// So far so good shapes render as expected. Next put the shapes in a group
const group = new paper.Group([bg,shape])
// rasterise the group (thinking it needs to be rasterized to get the pixel data, but again , not sure?)
group.rasterize()
// iterate over each pixel on the canvas and get the image data
for(let x = 0; x < width; x++){
for(let y = 0; y < height; y++){
const { data } = view.context.getImageData(x,y,1,1)
console.log(data)
}
}
Expecting: To get an array of buffers where if the pixel is white it would give me
Uint8ClampedArray(4) [0, 0, 0, 0, buffer: ArrayBuffer(4),
byteLength: 4, byteOffset: 0, length: 4]
0: 255
1: 255
2: 255
//(not sure if the fourth index represents (rgb'a')?
3: 255
buffer:
ArrayBuffer(4)
byteLength: 4
byteOffset: 0
length: 4
Symbol(Symbol.toStringTag): (...)
[[Prototype]]: TypedArray
and if the pixel is black I should get
Uint8ClampedArray(4) [0, 0, 0, 0, buffer: ArrayBuffer(4),
byteLength: 4, byteOffset: 0, length: 4]
0: 0
1: 0
2: 0
3: 0
buffer:
ArrayBuffer(4)
byteLength: 4
byteOffset: 0
length: 4
Symbol(Symbol.toStringTag): (...)
[[Prototype]]: TypedArray
i.e either 255,255,255 (white) or 0,0,0(black)
Instead, all the values are 0,0,0?
I think that your issue was that at the time where you are getting the image data, your scene is not yet drawn to the canvas.
In order to make sure it's drawn, you just need to call view.update().
Here's a simple sketch demonstrating how it could be used.
Note that you don't need to rasterize your scene if you are using the Canvas API directly to manipulate the image data. But you could also rasterize it and take advantage of Paper.js helper methods like raster.getPixel().
// Draw a white background (you effectively need it otherwise your default
// pixels will be black).
new Path.Rectangle({
rectangle: view.bounds,
fillColor: 'white'
});
// Draw a black rectangle covering most of the canvas.
new Path.Rectangle({
rectangle: view.bounds.scale(0.9),
fillColor: 'black'
});
// Make sure that the scene is drawn into the canvas.
view.update();
// Get the canvas image data.
const { width, height } = view.element;
const imageData = view.context.getImageData(0, 0, width, height);
// Loop over each pixel and store all the different colors to check that this works.
const colors = new Set();
const length = imageData.data.length;
for (let i = 0; i < length; i += 4) {
const [r, g, b, a] = imageData.data.slice(i, i + 4);
const color = JSON.stringify({ r, g, b, a });
colors.add(color);
}
console.log('colors', [...colors]);

What does spread operator in type declaration do in Flow?

If I have two objects of partially matching shapes like
const point2d = { x: 0, y: 0 };
const point3d = { x: 0, y: 0, z: 0 };
then the valid type declaration in Flow would be
type Point2D = { x: number, y: number };
type Point3D = Point2D & { z: number };
At first, I tried to use the object spread operator and hit a problem quite soon because notation like
type Point3D = { ...Point2D, z: number };
is passed as valid but does not achieve the goal because in the end both x and y properties are missing from the Point3D type.
For example, I can do this with spread notation (which is wrong):
type Point2D = { x: number, y: number };
type Point3D = { ...Point2D, z: number };
const point2d: Point2D = { x: 0, y: 0 };
const point3d: Point3D = { y: 0, z: 0 }; // No errors
but cannot miss the x property in object declaration with type intersection notation:
type Point2D = { x: number, y: number };
type Point3D = Point2D & { z: number };
const point2d: Point2D = { x: 0, y: 0 };
const point3d: Point3D = { y: 0, z: 0 }; // Cannot assign object literal to `point3d` because property `x` is missing in object literal [1] but exists in `Point2D` [2].
Note that both cases are not exact shapes.
Is Flow's behavior in case of spread notation intentional in this case? Am I missing something?
See this issue.
The short version is that you can resolve basically all of these sorts of issues by making your objects exact. In general I've found that you'll have a much easier time with object types when making them exact as a rule unless you really don't want them to be exact for some reason. Also, $ReadOnly where applicable. Try

Transform 2D context in QML Canvas

I am drawing part arc in circle with QML Canvas.
This is my code:
import QtQuick 2.0
import QtQml 2.2
Item {
id: root
property real arcAzimuth: 0
property real arcAngle: 80
property string arcColor: "red"
rotation: - (arcAngle / 4)
onArcColorChanged: canvas.requestPaint()
onArcAngleChanged: canvas.requestPaint()
Canvas {
id: canvas
anchors.fill: parent
rotation: -90 + parent.rotation
onPaint: {
var ctx = getContext("2d")
var x = width / 2
var y = height / 2
var start = Math.PI * (parent.arcAzimuth / 180)
var end = Math.PI * ((parent.arcAzimuth + parent.arcAngle) / 180)
ctx.reset()
ctx.beginPath();
ctx.lineWidth = 8
ctx.arc(x, y, (width / 2) - ctx.lineWidth / 2, start, end, false)
ctx.strokeStyle = root.arcColor
ctx.stroke()
}
}
}
This draws me something like angle of unfilled circle (border of circle). I want to draw exact same thing, but I want to rotate this by something like z coord so it will look like standing and looking on circle that is painted on floor.
How can I do this?
(After imgur will start working with stackoverflow, i will provide images)
Thank for your help
//Edit: Temporaly images links (because of error with uploading)
I have got this
and I want this
If you want to obtain a rotation in several axes you must pass a Rotation to transform:
import QtQuick 2.0
import QtQml 2.2
Item {
id: root
property real arcAzimuth: 0
property real arcAngle: 80
property string arcColor: "red"
rotation: - (arcAngle / 4)
onArcColorChanged: canvas.requestPaint()
onArcAngleChanged: canvas.requestPaint()
Canvas {
id: canvas
anchors.fill: parent
transform: Rotation{
axis { x: 0; y: 0.8; z: 1.0 }
angle: 225 + parent.rotation
}
onPaint: {
var ctx = getContext("2d")
var x = width / 2
var y = height / 2
var start = Math.PI * (parent.arcAzimuth / 180)
var end = 2*Math.PI * ((parent.arcAzimuth + parent.arcAngle) / 180)
ctx.reset()
ctx.beginPath();
ctx.lineWidth = 8
ctx.arc(x, y, (width / 2) - ctx.lineWidth / 2, start, end, false)
ctx.strokeStyle = root.arcColor
ctx.stroke()
}
}
}

Qt (QML) Dashed Circle

Is there any way to draw half dashed circle in QML? I drawn half circle in this way
var Circle = getContext("2d");
Circle.save();
var CircleGradient =
Circle.createLinearGradient(parent.width/4,parent.height,parent.width/4,0);
CircleGradient.addColorStop(0, firstGradientPoint);
CircleGradient.addColorStop(1, secondGradientPoint);
Circle.clearRect(0, 0, parent.width, parent.height);
Circle.beginPath();
Circle.lineCap = "round";
Circle.lineWidth = 10;
Circle.strokeStyle = CircleGradient
Circle.arc(parent.width/2, parent.height/2, canvas.radius - (Circle.lineWidth / 2), Math.PI/2, canvas.Value);
Circle.stroke();
Circle.restore();
Result
But how can I make it dashed like this.
I need
I know that this question is very outdated, but it might help someone. You can use Qt Quick Shapes (since Qt 5.10) to render what you want. It's not copy-paste code, but more of an approach:
Shape {
ShapePath {
id: shapePath
strokeColor: "black"
strokeStyle: ShapePath.DashLine
dashPattern: [6, 8]
fillColor: "transparent"
PathArc {
x: 0
y: radiusX + radiusY
radiusX: 100
radiusY: 100
useLargeArc: true
}
}
}
PathArc documentation has pretty much everything you need. Here are some more Shape Examples.
I know QML little bit but never coded.
But you can solve your problem by logic.
Here is the logic- Code below is pseudo, will not work but will give you an idea.
Draw the small arcs in loop with spaces in between.
//DECLARE YOUR ANGLES START AND END
startAngle = 0.0;
endAngle = pi/20;// 10 ARCS AND 10 SPACES
while (q++ < 10){
Circle.arc(parent.width/2, parent.height/2, canvas.radius - (Circle.lineWidth / 2), startAngle, endAngle, canvas.Value)
//LEAVE SPACE AND CREATE NEW START AND END ANGLE.
startAngle = endAngle + endAngle;
endAngle = startAngle + endAngle;
}

Canvas and unit/normal gradients

I am trying to use unit/normal vector based gradients in html5 canvas element and transform them afterwards for the desired results. However, I seem to figure troubles which might be because of my lack of math. I am trying to create a simple linear gradient going from 0,0 to 1,0 (i.e. a simple unit gradient going from left to right). Afterwards, I transform the canvas for scaling, rotating and moving the gradient. However, when for example giving a rotation value of 45DEG, the actual gradient gets painted wrong. The right bottom corner has way to much black that is, the gradient seems to be not "big" enough. Here's my code:
var rect = {x: 0, y: 0, w: 500, h: 500};
var rotation = 45 * Math.PI/180;
var sx = 1;
var sy = 1;
var tx = 0;
var ty = 0;
var radial = false;
// Create unit vector 0,0 1,1
var grd = radial ? ctx.createRadialGradient(0, 0, 0, 0, 0, 0.5) : ctx.createLinearGradient(0, 0, 1, 0);
grd.addColorStop(0, 'black');
grd.addColorStop(0.1, 'lime');
grd.addColorStop(0.9, 'yellow');
grd.addColorStop(1, 'black');
// Add our rectangle path before transforming
ctx.beginPath();
ctx.moveTo(rect.x, rect.y);
ctx.lineTo(rect.x + rect.w, rect.y);
ctx.lineTo(rect.x + rect.w, rect.y + rect.h);
ctx.lineTo(rect.x, rect.y + rect.h);
ctx.closePath();
// Rotate and scale unit gradient
ctx.rotate(rotation);
ctx.scale(sx * rect.w, sy * rect.h);
ctx.fillStyle = grd;
// Fill gradient
ctx.fill();
And here's the fiddle to try it out:
http://jsfiddle.net/4GsCE/1/
Curious enough, changing the unit linear gradient vector to a factor of about 1.41 makes the gradient look right:
ctx.createLinearGradient(0, 0, 1.41, 0)
Which can be seen in this fiddle:
http://jsfiddle.net/4GsCE/2/
But I couldn't figure how to calculate that factor?
Since you want to use normalized gradients, you have to decide how to normalize. Here you choose to center the gradient, and to have its (x,y) in the [-0.5, 0.5 ] range.
First issue is that the linear gradient is not centered, it's in the [0, 1.0] range.
Normalize them the same way :
var linGrd = ctx.createLinearGradient(-0.5, 0, 0.5, 0);
Second issue is that you must translate to the center of your figure, then scale, then draw in a normalized way.
Meaning you must use same coordinate system as your gradients.
Since you were both drawing a shape having (w,h) as size AND using a scale of (w,h), you were drawing a ( ww, hh ) sized rect.
Correct draw code is this one :
function drawRect(rect, fill) {
ctx.save();
// translate to the center of the rect (the new (0,0) )
ctx.translate(rect.x + rect.w / 2, rect.y + rect.h / 2);
// Rotate
ctx.rotate(rotation);
// scale to the size of the rect
ctx.scale(rect.w, rect.h);
// ...
ctx.fillStyle = fill;
// draw 'normalized' rect
ctx.fillRect(-0.5, -0.5, 1, 1);
ctx.restore();
}
Notice that by default the radialGradient will end at a distance of 0.5, meaning, if you are filling a rect, that it will fill the corners with the last color of the gradient. Maybe you want the corners to end the gradient.
In that case, you want to have the gradient to reach its value at a distance of :
sqrt ( 0.5*0.5 + 0.5*0.5 ) = 0.7 ( pythagore in the normalized circle)
So you'll define your normalized gradient like :
var fullRadGrd = ctx.createRadialGradient(0, 0, 0, 0, 0, 0.7) ;
http://jsfiddle.net/gamealchemist/4GsCE/4/

Resources