I'm trying to implement rotary input scrolling (currently with Galaxy Watch 4, so the bezel controls rotary input) on a Horizontal Pager. I can get it to move forward, but not backwards, no matter what direction I move the bezel in. How do I detect counter clockwise rotary to make the pager go back instead of forward?
Note:
pagerState.scrollBy(it.horizontalScrollPixels) does work forwards and backwards but doesn't snap to the next page, only slowly scrolls partially. This can be solved this way too (barring janky animation, animateToPage flows better, but presents the same lack of backwards scroll issue). I will accept an answer that can get a value to snap to the next page for all different screen sizes centered using scrollBy. I'm thinking it's it.horizontalScrollPixels times something ("it" is a RotaryScrollEvent object)
This code moves the pager forward
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
HorizontalPager(
count = 4,
state = pagerState,
// Add 32.dp horizontal padding to 'center' the pages
modifier = Modifier.fillMaxSize().onRotaryScrollEvent {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.targetPage, 1f)
}
true
}
.focusRequester(focusRequester)
.focusable()
You should check how the direction and size of the scroll event.
Also scroll by changing the target page, not the offset within that page. You code happens to work because scrolling to 1.0 in the page moves to the next page.
/**
* ScrollableState integration for Horizontal Pager.
*/
public class PagerScrollHandler(
private val pagerState: PagerState,
private val coroutineScope: CoroutineScope
) : ScrollableState {
override val isScrollInProgress: Boolean
get() = totalDelta != 0f
override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
private var totalDelta = 0f
private val scrollableState = ScrollableState { delta ->
totalDelta += delta
val offset = when {
// tune to match device
totalDelta > 40f -> {
1
}
totalDelta < -40f -> {
-1
}
else -> null
}
if (offset != null) {
totalDelta = 0f
val newTargetPage = pagerState.targetPage + offset
if (newTargetPage in (0 until pagerState.pageCount)) {
coroutineScope.launch {
pagerState.animateScrollToPage(newTargetPage, 0f)
}
}
}
delta
}
override suspend fun scroll(
scrollPriority: MutatePriority,
block: suspend ScrollScope.() -> Unit
) {
scrollableState.scroll(block = block)
}
}
val state = rememberPagerState()
val pagerScrollHandler = remember { PagerScrollHandler(state, coroutineScope) }
modifier = Modifier
.fillMaxSize()
.onRotaryScrollEvent {
coroutineScope.launch {
pagerScrollHandler.scrollBy(it.verticalScrollPixels)
}
true
}
.focusRequester(viewModel.focusRequester)
.focusable()
Also you should check that targetPage + offset is a valid page.
I tested this on a Galaxy Watch 4. Using this guide in the official documentation (https://developer.android.com/training/wearables/user-input/rotary-input#kotlin) I printed the delta values when I scrolled using bezel of the watch. For each scroll that I made i clockwise direction I got a delta of 128 and -128 for each counterclockwise scroll.
Using simple if else blocks I was able to distinguish when I scrolled above and below.
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) {
runBlocking {
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(baseContext), baseContext
)
if (delta < 127) {
scalingLazyListState.scrollBy(-100f)
} else {
scalingLazyListState.scrollBy(100f)
}
}
}
return super.onGenericMotionEvent(event)
}
Related
In this gesture multi-point control demo, it should be zooming and rotating with two fingers, but I found that one finger can be dragged in the test.
I want to drag with two fingers, how can I modify it?
#Composable
fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
Modifier.transformable doesn't have much customization options, you can copy source code file to your project and replace
val panChange = event.calculatePan()
With
val panChange = if (event.changes.count() > 1) event.calculatePan() else Offset.Zero
You can create a Modifier as
Modifier
.pointerInput(Unit) {
awaitEachGesture {
// Wait for at least one pointer to press down
awaitFirstDown()
do {
val event = awaitPointerEvent()
// You can set this as required
if(event.changes.size==2){
offset = event.calculatePan()
// zoom
event.calculateZoom()
// rotation
event.calculateRotation()
}
// This is for preventing other gestures consuming events
// prevent scrolling or other continuous gestures
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consume()
}
} while (event.changes.any { it.pressed })
}
}
I also wrote a library that returns transform values with start, move, and end callbacks and number of pointer down that you can also use
https://github.com/SmartToolFactory/Compose-Extended-Gestures
Modifier.pointerInput(Unit) {
detectTransformGestures(
onGestureStart = {
transformDetailText = "GESTURE START"
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
mainPointerInputChange: PointerInputChange,
pointerList: List<PointerInputChange> ->
},
onGestureEnd = {
borderColor = Color.LightGray
transformDetailText = "GESTURE END"
}
)
}
Or this one that checks number of pointers and prequsite condition before returning zoom, rotate, drag and so on
Modifier
.pointerInput(Unit) {
detectPointerTransformGestures(
numberOfPointers = 1,
requisite = PointerRequisite.GreaterThan,
onGestureStart = {
transformDetailText = "GESTURE START"
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
numberOfPointers: Int ->
},
onGestureEnd = {
transformDetailText = "GESTURE END"
}
)
}
I have two styles of interactions, one highlights the feature, the second places a tooltop with the feature name. Commenting both out, they're very fast, leave either in, the map application slows in IE and Firefox (but not Chrome).
map.addInteraction(new ol.interaction.Select({
condition: ol.events.condition.pointerMove,
layers: [stationLayer],
style: null // this is actually a style function but even as null it slows
}));
$(map.getViewport()).on('mousemove', function(evt) {
if(!dragging) {
var pixel = map.getEventPixel(evt.originalEvent);
var feature = null;
// this block directly below is the offending function, comment it out and it works fine
map.forEachFeatureAtPixel(pixel, function(f, l) {
if(f.get("type") === "station") {
feature = f;
}
});
// commenting out just below (getting the feature but doing nothing with it, still slow
if(feature) {
target.css("cursor", "pointer");
$("#FeatureTooltip").html(feature.get("name"))
.css({
top: pixel[1]-10,
left: pixel[0]+15
}).show();
} else {
target.css("cursor", "");
$("#FeatureTooltip").hide();
}
}
});
I mean this seems like an issue with OpenLayers-3 but I just wanted to be sure I wasn't overlooking something else here.
Oh yeah, there's roughly 600+ points. Which is a lot, but not unreasonably so I would think. Zooming-in to limit the features in view definitely helps. So I guess this is a # of features issue.
This is a known bug and needs more investigation. You can track progress here: https://github.com/openlayers/ol3/issues/4232.
However, there is one thing you can do to make things faster: return a truthy value from map.forEachFeatureAtPixel to stop checking for features once one was found:
var feature = map.forEachFeatureAtPixel(pixel, function(f) {
if (f.get('type') == 'station') {
return feature;
}
});
i had same issue, solved a problem by setInterval, about this later
1) every mouse move to 1 pixel fires event, and you will have a quee of event till you stop moving, and the quee will run in calback function, and freezes
2) if you have an objects with difficult styles, all element shown in canvas will take time to calculate for if they hit the cursor
resolve:
1. use setInterval
2. check for pixels moved size from preview, if less than N, return
3. for layers where multiple styles, try to simplify them by dividing into multiple ones, and let only one layer by interactive for cursor move
function mouseMove(evt) {
clearTimeout(mm.sheduled);
function squareDist(coord1, coord2) {
var dx = coord1[0] - coord2[0];
var dy = coord1[1] - coord2[1];
return dx * dx + dy * dy;
}
if (mm.isActive === false) {
map.unByKey(mm.listener);
return;
}
//shedules FIFO, last pixel processed after 200msec last process
const elapsed = (performance.now() - mm.finishTime);
const pixel = evt.pixel;
const distance = squareDist(mm.lastP, pixel);
if (distance > 0) {
mm.lastP = pixel;
mm.finishTime = performance.now();
mm.sheduled = setTimeout(function () {
mouseMove(evt);
}, MIN_ELAPSE_MSEC);
return;
} else if (elapsed < MIN_ELAPSE_MSEC || mm.working === true) {
// console.log(`distance = ${distance} and elapsed = ${elapsed} mesc , it never should happen`);
mm.sheduled = setTimeout(function () {
mouseMove(evt);
}, MIN_ELAPSE_MSEC);
return;
}
//while multithreading is not working on browsers, this flag is unusable
mm.working = true;
let t = performance.now();
//region drag map
const vStyle = map.getViewport().style;
vStyle.cursor = 'default';
if (evt.dragging) {
vStyle.cursor = 'grabbing';
}//endregion
else {
//todo replace calback with cursor=wait,cursor=busy
UtGeo.doInCallback(function () {
checkPixel(pixel);
});
}
mm.finishTime = performance.now();
mm.working = false;
console.log('mm finished', performance.now() - t);
}
In addition to #ahocevar's answer, a possible optimization for you is to utilize the select interaction's select event.
It appears that both the select interaction and your mousemove listener are both checking for hits on the same layers, doing double work. The select interaction will trigger select events whenever the set of selected features changes. You could listen to it, and show the popup whenever some feature is selected and hide it when not.
This should reduce the work by half, assuming that forEachFeatureAtPixel is what's hogging the system.
In my actionscript 3 i have a textfield, which i fill with the messages (get them from the webservice). Not all the messages are visible, so every (for Example) 5s i'm moving them vertically to the top.
To do that, i'm using the Tween effect, but the thing is, that it moves only the visible text and not the rest text together (the invisible one).
When scrolling - i can see the rest text.
Image to show the situation (Sorry for my skills):
Here are some code: Function to fill messages object. Plus count how many lines every message will take to display ( so i could easilly use the tween effect (From - to))
private function getNewMsg(e:ResultEvent):void
{
size = e.result.toString().split(" ").length - 1;
for (var i=0; i<size; i++)
{
smsList.push(new smsItem(e.result[i].Id, e.result[i].text));
}
summ = 0;
for (var d=0; d<size; d++)
{
textfield.appendText(smsList[d].Text.toUpperCase()+'\n');
if (d >= 1)
{
smsList[d].Lines = textfield.numLines - summ;
summ += smsList[d].Lines;
}
else
{
smsList[d].Lines = textfield.numLines-1;
summ += smsList[d].Lines + 1;
}
}
twEnd = smsList[1].Lines * (-30.5);
}
And on timer i'm starting the Tween:
function timerHandler(e:TimerEvent):void
{
myTweenX = new Tween(textfield, "y", None.easeIn, twStart, twEnd, 4, true)
myTweenX.addEventListener(TweenEvent.MOTION_FINISH, tweenCompleted);
}
function tweenCompleted(e:TweenEvent):void
{
twInd++;
twStart = twEnd;
twEnd += smsList[twInd].Lines * (-30.5);
}
Found the problem. Because of my fixed textfield height, text wasn't. After made the textfields height auto size, evrything got fixed.
Okay so I'm starting to make a main menu for a small flash game and to do this I want to use the mouse to click on buttons etc. I have a button class in which I create two rectangles: a rectangle for the button and a rectangle for the mouse based on its X and Y, 1 pixel by 1 pixel. I use Rectangle.Intersects to check if they are touching before seeing if left mouse button is down. Problem is, the thing the mouse position is relative to changes every time so no matter where the mouse button is on the screen, it's never the same co-ordinates as in a different build in that exact same position. I seriously just need ideas now as I'm running out. If I didn't explain it very well or you need further details to help please ask - I WOULD BE SO GRATEFUL.
Will post back if I find an answer
Update - Okay here's the button class
class OnScreenButton
{
public Texture2D texture;
Vector2 position;
Rectangle rectangle;
Color colour = new Color(255, 255, 255, 255);
public Vector2 size;
public OnScreenButton(Texture2D newtexture, GraphicsDevice graphics)
{
texture = newtexture;
// ScreenW = 500, ScreenH = 600
// Img W = 80, Img H = 20
size = new Vector2(graphics.Viewport.Width / 10, graphics.Viewport.Height / 30);
size = new Vector2(texture.Width, texture.Height);
}
bool down;
public bool isClicked;
public void Update(MouseState mouseState)
{
rectangle = new Rectangle((int)position.X, (int)position.Y, (int)size.X, (int)size.Y);
Rectangle mouseRectangle = new Rectangle(mouseState.X, mouseState.Y, 1, 1);
if (mouseRectangle.Intersects(rectangle))
{
if (colour.A == 255)
{
down = false;
}
if (colour.A == 0)
{
down = true;
}
if (down)
{
colour.A += 3;
}
else
{
colour.A -= 3;
}
if (mouseState.LeftButton == ButtonState.Pressed)
{
isClicked = true;
}
}
else if (colour.A < 255)
{
colour.A += 3;
isClicked = false;
colour.A = (255);
}
}
public void SetPosition(Vector2 newPos)
{
position = newPos;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, rectangle, colour);
}
}
}
(Sorry for weird formatting, brand new to stack overflow and the posting is still a little confusing)
Here is some other code I think is relevent...
Game.1 initializing stuff
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
protected override void Initialize()
{
// TODO: Add your initialization logic here
Mouse.WindowHandle = Window.Handle;
base.Initialize();
}
public Main()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
KeyboardState keyboardState;
MouseState mouseState;
Main menu update routine...
private void UpdateMainMenu(GameTime gameTime)
{
// Button options
if (buttonPlay.isClicked == true)
{
CreateNewGame();
currentGameState = GameState.playing;
}
buttonPlay.Update(mouseState);
if (buttonExit.isClicked == true)
{
this.Exit();
}
buttonExit.Update(mouseState);
// Press enter to play
if (keyboardState.IsKeyDown(Keys.Enter))
{
CreateNewGame();
currentGameState = GameState.playing;
}
}
Here's thee draw routine for main menu...
public void DrawMainMenu()
{
spriteBatch.Draw(mainMenuBackground, new Vector2(0, 0), Color.White);
buttonPlay.Draw(spriteBatch);
buttonExit.Draw(spriteBatch);
spriteBatch.DrawString(playerAmmoFont, String.Format("{0}", mouseState), new Vector2(0, 0), Color.White);
}
okay that's all I can think of
UPDATE - Okay so I know a few things that aren't the problem...
The whole of my button class is fine, I made a new project and inserted all the relevant code into it and it worked absolutely perfectly so I'm starting to think its something to do with the code positioning and the graphics device stuff although I still don't have a clue how to fix it.
the window appears at the same spot every time
there is no pattern to the change in coordinates at all
this is really annoying
UPDATE - OKAY. I spent a long time writing down the coordinates that I got each time I ran the code and stuck to cursor in the top right corner of the screen. Here is what I got.
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-78, -100)
(-128, -150)
(-178, -200)
(-228, -250)
(-28, -50)
(-53, -75)
(-103, -125)
(-153, -175) < AND FROM HERE THE PATTERN LOOPS ROUND.
I just don't get how the same code can execute a different bug on different executions like this.
Also, mouse.Wheel doesn't go up or down whereas it works on the project that I made to test the relevant code where the mouse position was relevant to the top left pixel of the game window.
UPDATE - EVEN MORE DAMN COMPLICATIONS - So I just rand it a few times again and the offset values are offset... the increase is the same but I got values like (-178, -200) then (-228, -250). I have also discovered that the mouse is not relative to the game window what so ever, if I jam the mouse in the top right corner of the screen and check the coordinates, then move the game window and do the same again, the coordinates don't change. Please please please help me, or tell me if I'm being stupid, or something. Thanks.
The mouse coordinates are relative to the monitor. Here is my general button class to try and work for your situation.
public class Button
{
public event EventHandler<EventArgs> Clicked;
public Vector2 Position { get; set;}
public Texture2D Texture { get; set;}
public Color Tint { get; set; }
public float Scale { get; set; }
public float Rotation { get; set; }
public int Width
{
get
{
if (texture == null)
return 0;
else
return texture.Width;
}
}
public int Height
{
get
{
if (texture == null)
return 0;
else
return texture.Height;
}
}
private void OnClick()
{
if (Clicked != null)
Clicked(this, EventArgs.Empty);
}
public Button(Vector2 position, Texture2D texture)
: base(parent)
{
Position = position;
Texture = texture;
Tint = Color.White;
Scale = 1.0f;
Rotation = 0.0f;
}
public bool HandleClick(Vector2 vector)
{
if (vector.X >= Position.X)
{
if (vector.X <= Position.X + Width)
{
if (vector.Y >= Position.Y)
{
if (vector.Y <= Position.Y + Height)
{
OnClick();
return true;
}
}
}
}
return false;
}
public bool HandleEntered(Vector2 vector)
{
if (vector.X >= Position.X)
{
if (vector.X <= Position.X + Width)
{
if (vector.Y >= Position.Y)
{
if (vector.Y <= Position.Y + Height)
{
return true;
}
}
}
}
return false;
}
public override void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(Texture, Position, null, Tint, Rotation, Vector2.Zero, Scale, SpriteEffects.None, 0.0f);
}
Declare a button:
Button btn = new Button(position where you want the button, texture for the button);
btn.Clicked += () => { /* Handle button clicked code here */ };
In your update method inside your main game:
public void Update (GameTime gameTime)
{
MouseState mouseState = Mouse.GetState();
if(mouseState.LeftButton == ButtonState.Pressed) // check if mouse is clicked
{
btn.HandleClicked(new Vector2(mouseState.X, mouseState.Y)); // If true then the button clicked event will fire
// Here you can also change the color of the button if the button is currently clicked
}
// Here you can change the color of the button if the mouse is hover over the control
// Example:
btn.Tint = btn.HandleEntered(new Vector2(mouseState.X, mouseState.Y)) ? Color.White * 0.75f : Color.White;
}
Note: You can also use a rectangle for the button to adjust its size instead of strictly using the textures dimensions. Hope this gives some insight.
So here's what was going on: I had a bullet class in my game for every bullet shot. In this class I check whether the bullets hits nothing, hits the asteroid, or destroys the asteroids. If the latter is true then I would increment playerScore by 5. PlayerScore was a Game1 attribute so I thought the easiest way to do this would be to create a new Game1 in bullet.cs to allow me to refer to the variable. Deleting the "Main mainGame = new Main():" in Bullet.cs fixed this issue and I think the issue was coming from a new graphics device being made every single time I fired a single bullet.
In my app, I use a UIPopoverController and I use the presentPopoverFromRect API. What I am doing now is just setting it to the frame of my whole UISegmentedControl. However I want to be more precise than this. Is there any way to get the frame of a specific index in the UISegmentedControl?
Thanks!
For our project we needed the actual frames of each segment, frame division wasn't enough. Here's a function I wrote that calculates the exact frame for every segment. Be aware that it accesses the segmented control actual subviews, so it might break in any iOS update.
- (CGRect)segmentFrameForIndex:(NSInteger)index inSegmentedControl:(UISegmentedControl *)control
{
// WARNING: This function gets frame from UISegment objects, undocumented subviews of UISegmentedControl.
// May break in iOS updates.
NSMutableArray *segments = [NSMutableArray arrayWithCapacity:self.numberOfSegments];
for (UIView *view in control.subviews) {
if ([NSStringFromClass([view class]) isEqualToString:#"UISegment"]) {
[segments addObject:view];
}
}
NSArray *sorted = [segments sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
if (a.frame.origin.x < b.frame.origin.x) {
return NSOrderedAscending;
} else if (a.frame.origin.x > b.frame.origin.x) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
return [[sorted objectAtIndex:index] frame];
}
If the segments are equal, why not just divide the width of the control by the number of the selected segment (+1 because numbering starts at 0)?
EDIT: Like this
-(void)showPopover:(id)sender {
if ((UISegmentedControl*)sender.selectedSegmentIndex == 0)
[self.popover presentPopoverFromRect:CGRectMake(self.segmentedControl.frame.size.width/6, self.segmentedControl.frame.origin.y, aWidth, aHeight)]
}
It's over 6 (I'm assuming a 3 segment implementation), because you have to get the center of the segment, and 3 would put it on the lines. And if you do some simple math here (let's assume the whole control is 60 px wide), then 60/3 yeilds 20. Because each segment is 20 px wide, the width of 60 over six yields the correct answer 10.
This can be done much easier without matching class names, providing that you use no custom segmented control (Swift version)
extension NSSegmentedControl {
func frameForSegment(segment: Int) -> NSRect {
var left : CGFloat = 0
for i in 0..<segmentCount {
let w = widthForSegment(i)
if i == segment {
let off = CGFloat(i) + 2 // Account for separators and border.
return NSRect(x: left + off, y: bounds.minY, width: w, height: bounds.height)
}
left += w
}
return NSZeroRect
}
}
If somebody is searching for a solution for NSSegmentedControl, I've slightly modified #erik-aigner's answer. For UISegmentedControl it should work similarly.
This version improves the geometry computation for spacing and the code in general. Disclaimer: It was tested only for a segmented control placed in a toolbar.
import AppKit
public extension NSSegmentedControl {
/// The width of a single horizontal border.
public static let horizontalBorderWidth: CGFloat = 3
/// The height of a single vertical border.
public static let verticalBorderWidth: CGFloat = 2
/// The horizontal spacing between segments.
public static let horizontalSegmentSpacing: CGFloat = 1
/// Returns the frame of the specified segment.
///
/// - Parameter segment: The index of the segment whose frame should be computed.
/// - Returns: The frame of the segment or `.zero` if an invalid segment index is passed in.
public func frame(forSegment segment: Int) -> NSRect {
let y = bounds.minY - NSSegmentedControl.verticalBorderWidth
let height = bounds.height
var left = NSSegmentedControl.horizontalBorderWidth
for index in 0..<segmentCount {
let width = self.width(forSegment: index)
if index == segment {
return NSRect(x: left, y: y, width: width, height: height)
}
left += NSSegmentedControl.horizontalSegmentSpacing
left += width
}
return .zero
}
}
Swift 5.3 version of #CodaFi's answer:
func showPopover(_ sender: Any?) {
if sender?.selectedSegmentIndex as? UISegmentedControl == 0 {
popover.presentPopover(fromRect: CGRect(x: segmentedControl.frame.size.width / 6, y: segmentedControl.frame.origin.y, width: aWidth, height: aHeight))
}
}
For me widthForSegment returns 0, so the other answers always fail to produce frame. I think a better solution is to just get the frame of a subview:
public extension UISegmentedControl {
func frame(forSegment segment: Int) -> CGRect {
subviews[segment].frame
}
}