Does anyone know how to create an alphabetical fast scroller, displaying all the letters in Jetpack Compose?
Similar to this one:
Recyclerview Alphabetical Scrollbar
I have made a list which is scrollable, but I have no clue in how to make the letters on the side and make it "jump" to the right letter.
here is my coding so far:
package com.example.ValpeHagen.ui.theme
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.ValpeHagen.model.DataProvider
import com.example.ValpeHagen.model.DataProvider2
import com.example.androiddevchallenge.data.model.Rase
class Valpen {
val rase = DataProvider2.rase
#Composable
fun VerticalHorizontalScroll(rase: Rase) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(Grass),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Velg Rase",
color = Color.White,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,)
}
LazyColumn {
item {
Text(
text = "Hurtigvalg",
color = Color.Black,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp)
)
}
//Horizontal Scroll view
item {
LazyRow {
itemsIndexed(items = DataProvider.puppyList) { index, itemPuppy ->
Card(
modifier = Modifier
.width(110.dp)
.height(140.dp)
.padding(10.dp, 5.dp, 5.dp, 0.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.White),
elevation = 5.dp
) {
Column(
modifier = Modifier.padding(5.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = DataProvider.puppy.puppyImageId),
contentDescription = "profile Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = DataProvider.puppy.breeds,
color = Color.Black,
fontWeight = FontWeight.Bold,
fontSize = 16.sp)
}
}
}
}
}
val mylist = listOf("Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog","Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog","Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog","Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog","Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog","Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog","Affenpinscher", "Wheaten terrier" , "dachshund",
"Fransk bulldog")
item {
Text(
text = "Alle hunderaser",
color = Color.Black,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(vertical = 10.dp, horizontal = 10.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.White))
}
item {
val list = listOf("A", "B", "C", "D",
"E","F","G","H","I","J","K","L","M","N",
"O","P","Q","R","S","T","U","V","W","X", "Y","Z","Æ","Ø","Å")
val expanded = remember { mutableStateOf(false) }
val currentValue = remember { mutableStateOf(list[0]) }
Surface(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier
.clickable {
expanded.value = !expanded.value
}
.align(Alignment.CenterStart)) {
Text(text = currentValue.value,
color = Grass,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 14.dp)
)
Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = null)
DropdownMenu(expanded = expanded.value, onDismissRequest = {
expanded.value = false
}) {
list.forEach {
DropdownMenuItem(onClick = {
currentValue.value = it
expanded.value = false
}) {
Text(text = it,
color = Grass,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(vertical = 10.dp,
horizontal = 10.dp))
}
}
}
}
}
}
}
}
}
}
}
To display letters all you need is a Column with needed items. To scroll a lazy column to a needed item, you can use lazy column state.
Here's a basic example:
val items = remember { LoremIpsum().values.first().split(" ").sortedBy { it.lowercase() } }
val headers = remember { items.map { it.first().uppercase() }.toSet().toList() }
Row {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.weight(1f)
) {
items(items) {
Text(it)
}
}
val offsets = remember { mutableStateMapOf<Int, Float>() }
var selectedHeaderIndex by remember { mutableStateOf(0) }
val scope = rememberCoroutineScope()
fun updateSelectedIndexIfNeeded(offset: Float) {
val index = offsets
.mapValues { abs(it.value - offset) }
.entries
.minByOrNull { it.value }
?.key ?: return
if (selectedHeaderIndex == index) return
selectedHeaderIndex = index
val selectedItemIndex = items.indexOfFirst { it.first().uppercase() == headers[selectedHeaderIndex] }
scope.launch {
listState.scrollToItem(selectedItemIndex)
}
}
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
.background(Color.Gray)
.pointerInput(Unit) {
detectTapGestures {
updateSelectedIndexIfNeeded(it.y)
}
}
.pointerInput(Unit) {
detectVerticalDragGestures { change, _ ->
updateSelectedIndexIfNeeded(change.position.y)
}
}
) {
headers.forEachIndexed { i, header ->
Text(
header,
modifier = Modifier.onGloballyPositioned {
offsets[i] = it.boundsInParent().center.y
}
)
}
}
}
Related
The bounty expires in 6 days. Answers to this question are eligible for a +50 reputation bounty.
Bilal wants to draw more attention to this question.
I want to replicate the existent example of loading Collada kinematics and using them with my own model, to do so I have created a class as follows:
iimport { Object3D, MathUtils } from "three";
import { ColladaLoader } from "three/addons/loaders/ColladaLoader.js";
import yumi_path from "../assets/dae/yumi.dae";
import { Tween, Easing, update } from "#tweenjs/tween.js";
export class YuMiMotion {
constructor(scene, joint_vlaues) {
this.scene = scene;
this.tgt_jnt_vals = joint_vlaues;
this.loader = new ColladaLoader();
this.yumi_model = new Object3D();
this.kinematics;
this.kinematicsTween;
this.tweenParameters = {};
this.loadYuMi();
this.startMotion();
}
startMotion = () => {
if (this.kinematics == undefined) {
console.log("Kinematics Still not loaded!");
this.anim_frame_id1 = requestAnimationFrame(this.startMotion);
}
else {
console.log(this.kinematics);
this.setupTween();
cancelAnimationFrame(this.anim_frame_id1);
this.animate();
}
}
animate = () => {
update();
this.anim_frame_id2 = requestAnimationFrame(this.animate);
}
loadYuMi = async() => {
const onLoad = (result, yumi) => {
const model = result.scene.children[0];
yumi.add(model.clone(true));
yumi.traverse(function (child) {
if (child.isMesh) {
child.material.flatShading = true;
}
});
this.kinematics = result.kinematics;
};
await this.loader.load(yumi_path, (collada) =>
onLoad(collada, this.yumi_model, this.kinematics)
),
undefined,
function (error) {
console.log("An error happened", error);
};
this.yumi_model.position.set(0.0, 0.0, -6.0);
this.yumi_model.rotation.set(-Math.PI / 2, 0.0, -Math.PI / 2);
this.yumi_model.scale.set(20, 20, 20);
this.scene.add(this.yumi_model);
}
setupTween =() =>{
const duration = MathUtils.randInt( 1000, 5000 );
const target = {};
for (const prop in this.tgt_jnt_vals) {
const joint = this.kinematics.joints[ prop ];
const old = this.tweenParameters[ prop ];
const position = old ? old : joint.zeroPosition;
this.tweenParameters[ prop ] = position;
target[prop] = this.tgt_jnt_vals[prop]; //MathUtils.randInt( joint.limits.min, joint.limits.max );
// console.log('target:', prop, this.tgt_jnt_vals[prop]);
}
this.kinematicsTween = new Tween( this.tweenParameters ).to( target, duration ).easing( Easing.Quadratic.Out );
this.kinematicsTween.onUpdate( ( object )=> {
for ( const prop in this.kinematics.joints ) {
if ( prop in this.kinematics.joints) {
if ( ! this.kinematics.joints[ prop ].static ) {
this.kinematics.setJointValue( prop, object[ prop ] );
}
}
}
});
// console.log("Tween Parameters", this.tweenParameters);
// console.log("kinematics tween", this.kinematicsTween);
this.kinematicsTween.start();
setTimeout( this.setupTween, duration );
}
}
And I'm calling this class as follows:
let target_position = {
gripper_l_joint: 0.01,
gripper_l_joint_m: 0.01,
gripper_r_joint: 0.01,
gripper_r_joint_m: 0.01,
yumi_joint_1_l: 10.0,
yumi_joint_1_r: 10.0,
yumi_joint_2_l: 20.0,
yumi_joint_2_r: 20.0,
yumi_joint_3_l: 30.0,
yumi_joint_3_r: 30.0,
yumi_joint_4_l: 40.0,
yumi_joint_4_r: 40.0,
yumi_joint_5_l: 50.0,
yumi_joint_5_r: 50.0,
yumi_joint_6_l: 60.0,
yumi_joint_6_r: 60.0,
yumi_joint_7_l: 70.0,
yumi_joint_7_r: 70.0,
};
new YuMiMotion(this.scene, target_position);
I have created a repo to reproduce my example here.
Can you please tell me how can I Move my model properly to the desired joints position using Three.js and Tween.js? thanks in advance.
I decided to use an OutlinedTextField and DropdownMenu so the user can fill an amount and select a currency.
This looks pretty nice in the preview, but when this code is being run on the device (virtual or physical) the DropdownMenu is being squeezed on the right, and therefore, the dropdown menu isn't actionable anymore.
#Composable
fun Money() {
Row() {
Amount()
Currency()
}
}
#Preview
#Composable
fun Currency() {
var mExpanded by remember { mutableStateOf(false) }
val mCurrencies = listOf("USD", "CHF", "EUR", "MAD") //, "Hyderabad", "Bengaluru", "PUNE")
var mSelectedText by remember { mutableStateOf("") }
var mTextFieldSize by remember { mutableStateOf(Size.Zero) }
val icon = if (mExpanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown
OutlinedTextField(
value = mSelectedText,
onValueChange = { mSelectedText = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
// This value is used to assign to
// the DropDown the same width
mTextFieldSize = coordinates.size.toSize()
},
label = { Text("Currency") },
trailingIcon = {
Icon(icon, "contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
DropdownMenu(
expanded = mExpanded,
onDismissRequest = { mExpanded = false },
modifier = Modifier
.width(with(LocalDensity.current) { mTextFieldSize.width.toDp() })
) {
mCurrencies.forEach { label ->
DropdownMenuItem(onClick = {
mSelectedText = label
mExpanded = false
}) {
Text(text = label)
}
}
}
}
#Composable
fun Amount() {
var amount by remember {
mutableStateOf("")
}
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = amount,
onValueChange = { amount = it },
label = { Text(text = "Amount") },
singleLine = true,
trailingIcon = {
if (amount.isNotBlank())
IconButton(onClick = { amount = "" }) {
Icon(
imageVector = Icons.Filled.Clear,
contentDescription = ""
)
}
}
)
}```
I am trying to create a scene with react-three/fiber and react-three/drei. I want to use a PerspectiveCamera and be able to pan/zoom/rotate with the mouse, but I am also trying to add some buttons that can update the camera position and target in order to have different views (eg. top view, bottom view, side view, etc). I have achieved the latter part and my buttons seem to be working as I update the target x,y,z and position x,y,z using props.
The only problem is that the camera is not responding to the mouse so I only get a fixed camera position and target.
I have included all the scene codes below.
import React,{ useRef, useState, useEffect} from 'react'
import * as THREE from 'three';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { PerspectiveCamera, Icosahedron, OrbitControls } from '#react-three/drei'
import { Canvas, useThree } from "#react-three/fiber";
function VisualizationComponent(props) {
const width = window.innerWidth;
const height = window.innerHeight;
const [controls, setControls] = useState(null);
const [threeState, setThreeState] = useState(null);
const [treeStateInitialized, setThreeStateInitialized] = useState(false);
useEffect(()=>{
if(threeState){
_.forOwn(props.objects, (value, key) => {
threeState.scene.current.add(value);
});
}
return () => {
if(controls) controls.dispose();
}
},[])
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const { objects } = props
const prevState = usePrevious({objects});
const mainCamera = useRef();
useEffect(() => {
if(!threeState) return;
if (
!treeStateInitialized ||
shouldUpdateObjects(props.objects, prevState.objects)
) {
setThreeStateInitialized(true);
garbageCollectOldObjects();
addDefaultObjects();
_.forOwn(props.objects, (value, key) => {
threeState.scene.add(value);
});
}
})
const addDefaultObjects = () => {
if (threeState) {
var hemiLight = new THREE.HemisphereLight( 0xffffbb, 0x080820, 0.2 );
hemiLight.position.set( 0, 0, 1 );
threeState.scene.add( hemiLight );
}
}
const garbageCollectOldObjects = () => {
while (threeState && threeState.scene.children.length) {
const oldObject = threeState.scene.children[0];
oldObject.traverse((child) => {
if (child.geometry) {
child.geometry?.dispose();
if(child.material && Array.isArray(child.material)){
child.material.forEach(d => d.dispose());
}else{
child.material?.dispose();
}
}
});
threeState.scene.remove(oldObject);
}
}
const shouldUpdateObjects = (currentObjects,nextObjects) => {
const result = false;
let currentDigest = 1;
let nextDigest = 1;
_.forIn(currentObjects, (value, key) => {
currentDigest *= value.id;
});
_.forIn(nextObjects, (value, key) => {
nextDigest *= value.id;
});
return currentDigest !== nextDigest;
}
const hasAncestorWhichDisablesThreeJs = (element) => {
if (!element) return false;
let isEditable = false;
for (let i = 0; i < element.classList.length; i++) {
if (element.classList[i] === 'disable-threejs-controls') {
isEditable = true;
}
}
return isEditable ||
hasAncestorWhichDisablesThreeJs(element.parentElement);
}
const initializeScene = (state) => {
setThreeState(state);
addDefaultObjects();
}
return (
<div
id="threejs-controllers-div"
className='threejs-container'
onMouseOver={ (e) => {
const target = e.target;
if (!target || !controls) return true;
if (hasAncestorWhichDisablesThreeJs(target)) {
controls.enabled = false;
} else {
controls.enabled = true;
}
} }
>
<Canvas
className='threejs'
onCreated={ (state) => {initializeScene(state)}}
shadows={true}
gl={
{
'shadowMap.enabled' : true,
'alpha' : true
}
}
>
<PerspectiveCamera
makeDefault
ref={mainCamera}
position-x={props.cameraX || 0}
position-y={props.cameraY || -20}
position-z={props.cameraZ || 20}
up={[0, 0, 1]}
fov={ 15 }
aspect={ width / height }
near={ 1 }
far={ 10000 }
visible={false}
controls={controls}
/>
<OrbitControls
ref={controls}
camera={mainCamera.current}
domElement={document.getElementById("threejs-controllers-div")}
enabled={true}
enablePan={true}
enableZoom={true}
enableRotate={true}
target-x={props.targetX || 0}
target-y={props.targetY || 0}
target-z={props.targetZ || 0}
/>
</Canvas>
<div className='threejs-react-container'>
{ props.children }
</div>
</div>
)
}
VisualizationComponent.propTypes = {
children: PropTypes.node.isRequired,
objects: PropTypes.object.isRequired,
cameraX: PropTypes.number,
cameraY: PropTypes.number,
cameraZ: PropTypes.number,
targetX: PropTypes.number,
targetY: PropTypes.number,
targetZ: PropTypes.number,
};
export default withRouter(VisualizationComponent);
I am currently working on two Android projects, both of which use Jetpack Compose. Both projects use a self-developed library that contains custom composables.
My problem now is that I don't know how to reference project-specific text styles, images or other design characteristics in the library. The implementation of Material Design is unfortunately not sufficient for this.
To give an example:
Both app should have an error screen in case something goes wrong. The structure of the error screen should be the same in both apps. The error screen should contain an error message, and error image and a retry button. To avoid duplicate code i moved the error screen composable to the library. No problem at that point.
But the error screen should also adapt to the app theme. Currently i use a drawable in the error screen composable located inside the library module. The button using the MaterialTheme.typography.button style and the message uses MaterialTheme.typography.body1 style.
But this is not the behavior i want to achieve here. I want to use different error images for both apps and the button and message styles could be completely different depending on the app which is using the composable.
Is there any clever way to achieve this behavior?
Edit: I used the error screen as an easy to understand example for this question, but did not implemented it in the library. But I implemented a profile composable where I am facing the exact same problem. Currently I am using the approach to pass the text styles as parameters, but this is getting ugly really fast:
#Composable
fun Profile(
modifier: Modifier = Modifier,
user: User,
sections: List<Section>,
userIsLoggedIn: Boolean,
onLogin: () -> Unit,
onLogout: () -> Unit,
loginButtonTextStyle: TextStyle = MaterialTheme.typography.button.copy(
textAlign = TextAlign.Center
),
logoutButtonTextStyle: TextStyle = MaterialTheme.typography.button.copy(
color = MaterialTheme.colors.onSurface,
textAlign = TextAlign.Center
),
usernameTextStyle: TextStyle = MaterialTheme.typography.h5.copy(
color = MaterialTheme.colors.onSurface,
textAlign = TextAlign.Center
),
sectionTitleTextStyle: TextStyle = MaterialTheme.typography.subtitle1.copy(
color = MaterialTheme.colors.onSurface
),
itemTitleTextStyle: TextStyle = MaterialTheme.typography.body1,
) {
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
.padding(top = 24.dp)
) {
if (userIsLoggedIn) {
UsernameHeader(
username = user.username,
textStyle = usernameTextStyle
)
} else {
LoginButton(
onLogin = onLogin,
textStyle = loginButtonTextStyle,
)
}
SectionsContainer(
sections = sections,
sectionTitleTextStyle = sectionTitleTextStyle,
itemTitleTextStyle = itemTitleTextStyle
)
if (userIsLoggedIn) {
LogoutButton(
onLogout = onLogout,
textStyle = logoutButtonTextStyle
)
}
}
}
#Composable
private fun UsernameHeader(
username: String,
textStyle: TextStyle
) {
Text(
text = "Hello $username",
style = textStyle,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
}
#Composable
private fun LoginButton(
textStyle: TextStyle,
onLogin: () -> Unit
) {
Button(
onClick = onLogin,
modifier = Modifier
.padding(
start = 16.dp,
end = 16.dp,
)
) {
Text(
text = "LOGIN",
style = textStyle,
modifier = Modifier
.fillMaxWidth()
.padding(top = 7.dp, bottom = 9.dp)
)
}
Spacer(modifier = Modifier.height(24.dp))
}
#Composable
private fun LogoutButton(
textStyle: TextStyle,
onLogout: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp)
) {
TextButton(
onClick = onLogout,
modifier = Modifier
.padding(
start = 24.dp,
end = 24.dp,
top = 7.dp,
bottom = 9.dp
)
) {
Text(
text = "LOGOUT",
style = textStyle,
)
}
}
}
#Composable
private fun SectionsContainer(
modifier: Modifier = Modifier,
sections: List<Section>,
sectionTitleTextStyle: TextStyle,
itemTitleTextStyle: TextStyle
) {
Column(modifier = modifier) {
for (section in sections) {
ProfileSection(
section = section,
titleTextStyle = sectionTitleTextStyle,
itemTitleTextStyle = itemTitleTextStyle,
)
}
}
}
#Composable
private fun ProfileSection(
modifier: Modifier = Modifier,
section: Section,
titleTextStyle: TextStyle,
itemTitleTextStyle: TextStyle,
) {
Column(
modifier.padding(bottom = 16.dp)
) {
Text(
text = section.title.toUpperCase(Locale.ROOT),
style = titleTextStyle,
modifier = Modifier
.padding(
start = 16.dp,
end = 16.dp,
top = 9.dp,
bottom = 10.dp
)
.fillMaxWidth()
)
for (item in section.items) {
ProfileItem(
item = item,
itemTitleTextStyle = itemTitleTextStyle
)
}
}
}
#Composable
private fun ProfileItem(
item: Item,
itemTitleTextStyle: TextStyle
) {
when (item) {
is TextItem -> ProfileTextItem(
textItem = item,
titleTextStyle = itemTitleTextStyle
)
is ImageItem -> ProfileImageItem(imageItem = item)
}
}
#Composable
private fun ProfileTextItem(
textItem: TextItem,
titleTextStyle: TextStyle
) {
Text(
text = textItem.title,
style = titleTextStyle,
modifier = Modifier
.padding(
start = 16.dp,
end = 16.dp,
top = 15.dp,
bottom = 12.dp
)
.fillMaxWidth()
)
}
#Composable
private fun ProfileImageItem(imageItem: ImageItem) {
Image(
painter = painterResource(id = imageItem.drawableResId),
contentDescription = imageItem.contentDescription,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.FillWidth
)
}
The models I used are quite simple:
Section:
data class Section(
val title: String,
val items: List<Item>
)
Item:
interface Item
data class TextItem(
val title: String
) : Item
data class ImageItem(
val drawableResId: Int,
val contentDescription: String?
) : Item
User:
data class User(
val username: String
)
I then use this profile composable inside an activity in both apps like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val systemUiController = remember { SystemUiController(window) }
CompositionLocalProvider(SystemUiControllerAmbient provides systemUiController) {
SampleApp()
}
}
}
#Composable
fun SampleApp() {
SampleTheme {
val systemUiController = SystemUiControllerAmbient.current
systemUiController.setSystemBarsColor(color = MaterialTheme.colors.surface)
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBarContainer() },
bottomBar = { BottomNavigationContainer() }
) { innerPadding ->
ProfileContainer(Modifier.padding(innerPadding))
}
}
}
#Composable
private fun TopAppBarContainer() {
TopAppBar(
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface
) {
Text(
text = "Profile",
style = MaterialTheme.typography.h2,
modifier = Modifier.padding(16.dp)
)
}
}
#Composable
private fun ProfileContainer(modifier: Modifier = Modifier) {
Surface(modifier = modifier) {
var userIsLoggedIn by remember { mutableStateOf(false) }
Profile(
Modifier.fillMaxWidth(),
user = User("Username"),
sections = fakeSections,
userIsLoggedIn = userIsLoggedIn,
onLogin = { userIsLoggedIn = true },
onLogout = { userIsLoggedIn = false }
)
}
}
#Composable
private fun BottomNavigationContainer() {
BottomNavigation(
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
) {
BottomNavigationItem(
selected = false,
onClick = { },
icon = {
Icon(
painter = painterResource(R.drawable.logo),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
},
label = {
Text(
text = "Home",
style = MaterialTheme.typography.caption,
color = grey60
)
}
)
BottomNavigationItem(
selected = false,
onClick = { },
icon = {
Icon(
painter = painterResource(R.drawable.ic_assortment),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
},
label = {
Text(
text = "Assortment",
style = MaterialTheme.typography.caption,
color = grey60
)
}
)
BottomNavigationItem(
selected = false,
onClick = { },
icon = {
Icon(
painter = painterResource(R.drawable.ic_heart),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
},
label = {
Text(
text = "Wishlist",
style = MaterialTheme.typography.caption,
color = grey60
)
}
)
BottomNavigationItem(
selected = false,
onClick = { },
icon = {
Icon(
painter = painterResource(R.drawable.ic_cart),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
},
label = {
Text(
text = "Cart",
style = MaterialTheme.typography.caption,
color = grey60
)
}
)
BottomNavigationItem(
selected = true,
onClick = { },
icon = {
Icon(
painter = painterResource(R.drawable.ic_user),
contentDescription = null,
tint = bonprixRed,
modifier = Modifier.size(24.dp)
)
},
label = {
Text(
text = "Profile",
style = MaterialTheme.typography.caption,
color = red
)
}
)
}
}
#Preview
#Composable
fun DefaultPreview() {
SampleApp()
}
}
I want to create a draggable / resizable / rotatable component in Ionic2.pan and pinch events are working great, but rotate has a strange behaviour: if I touch the component with two fingers, but without doing any kind of rotation, I will still get a rotation number around 15 to 30 deg, making the component rotate. I don't know if it is a known issue or something to do with the sensitivity of the screen. The code I am using for the component is this:
import { Component, ElementRef, Input, Renderer2 } from '#angular/core';
import { DomController, Gesture } from 'ionic-angular';
const defaultScale: number = 1;
const defaultRotation: number = 0;
#Component({
selector: 'draggable',
template: `
<ng-content></ng-content>
`
})
export class DraggableComponent {
#Input()
private position: {
x: number;
y: number;
};
#Input()
private dimensions: {
width: number;
height: number;
};
#Input()
private transform: {
scale: number;
rotation: number;
};
#Input()
protected container: any;
private gesture: Gesture;
private deltaCenter: {
x: number;
y: number;
} = null;
// when pinch + rotate, we will have very quick successive event when we release
private updating: boolean = false;
constructor(
private element: ElementRef,
private renderer: Renderer2,
private domCtrl: DomController
) {}
ngOnDestroy() {
this.gesture.destroy();
}
ngAfterViewInit() {
this.renderer.setStyle(this.element.nativeElement, 'position', 'absolute');
this.renderer.setStyle(this.element.nativeElement, 'transform-origin', 'center');
if (this.dimensions) {
if (this.dimensions.width) {
this.renderer.setStyle(this.element.nativeElement, 'width', this.dimensions.width + 'px');
}
if (this.dimensions.height) {
this.renderer.setStyle(this.element.nativeElement, 'height', this.dimensions.height + 'px');
}
}
if (!this.transform) {
this.transform = {
scale: 1,
rotation: 0
};
}
this.gesture = new Gesture(this.element.nativeElement);
this.gesture.listen();
this.gesture.on('pinch', this.handleGesture.bind(this));
this.gesture.on('rotate', this.handleGesture.bind(this));
this.gesture.on('panmove', this.handleGesture.bind(this));
this.gesture.on('pinchend panend rotateend', this.gestureEnd.bind(this));
this.updateStyles();
}
private handleGesture(event: {center: {y: number, x: number}, scale: number, rotation: number}) {
if (this.updating) {
return;
}
// even without doing any kind of rotation, using 2 fingers will set event.rotation between 15 to 30 degrees
if (!this.deltaCenter) {
this.deltaCenter = {
y: this.position.y - event.center.y,
x: this.position.x - event.center.x
};
}
this.position.y = event.center.y;
this.position.x = event.center.x;
this.updateStyles(event.scale, event.rotation);
}
private gestureEnd(event: {scale: number, rotation: number}) {
if (this.updating) {
return;
}
this.updating = true;
this.position.y += this.deltaCenter.y;
this.position.x += this.deltaCenter.x;
this.transform.scale = this.transform.scale * event.scale;
this.transform.rotation = this.transform.rotation + event.rotation;
this.deltaCenter = null;
this.updateStyles();
setTimeout(() => {
this.updating = false;
}, 100);
}
private get cntEle(): HTMLElement {
let cntEle: HTMLElement = null;
if (!this.container) {
return null;
}
else if (this.container instanceof Node) {
return this.container as HTMLElement;
}
else if (this.container.getNativeElement) {
return this.container.getNativeElement();
}
return null;
}
private get containerBoundingClientRect(): ClientRect {
if (this.cntEle) {
return this.cntEle.getBoundingClientRect();
}
else if (this.container && 'top' in this.container) {
return this.container as ClientRect;
}
// bound to whole document
return {
top: 0,
left: 0,
bottom: document.documentElement.clientHeight,
right: document.documentElement.clientWidth,
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
};
}
private get x(): number {
let x = this.position.x;
if (this.deltaCenter) {
x += this.deltaCenter.x;
}
if (x < this.containerBoundingClientRect.left) {
return this.containerBoundingClientRect.left;
}
else if (x > (this.containerBoundingClientRect.right - this.dimensions.width)) {
return this.containerBoundingClientRect.right - this.dimensions.width;
}
return x
}
private get y(): number {
let y = this.position.y;
if (this.deltaCenter) {
y += this.deltaCenter.y;
}
if (y < this.containerBoundingClientRect.top) {
return this.containerBoundingClientRect.top;
}
if (y > (this.containerBoundingClientRect.bottom - this.dimensions.height)) {
return this.containerBoundingClientRect.bottom - this.dimensions.height;
}
return y;
}
private updateStyles(scale: number = 1, rotation: number = 0) {
this.domCtrl.write(() => {
this.renderer.setStyle(this.element.nativeElement, 'top', this.y + 'px');
this.renderer.setStyle(this.element.nativeElement, 'left', this.x + 'px');
let transforms = [];
transforms.push(`scale(${this.transform.scale * scale})`);
transforms.push(`rotateZ(${this.transform.rotation + rotation}deg)`);
this.renderer.setStyle(this.element.nativeElement, 'transform', transforms.join(' '));
});
}
}