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

User profile screen UX/UI redesign #890

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ insert_final_newline = true
[*.{kt,kts,gradle}]
indent_size = 4
max_line_length = 120
disabled_rules=import-ordering
roihershberg marked this conversation as resolved.
Show resolved Hide resolved

[*.{yml,yaml}]
indent_size = 2
Expand Down
4 changes: 2 additions & 2 deletions atox/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ android {
multiDexEnabled = true
}
buildTypes {
getByName("debug") {
roihershberg marked this conversation as resolved.
Show resolved Hide resolved
debug {
applicationIdSuffix = ".debug"
}
getByName("release") {
release {
isMinifyEnabled = true
proguardFiles("proguard-tox4j.pro", getDefaultProguardFile("proguard-android-optimize.txt"))
}
Expand Down
2 changes: 1 addition & 1 deletion atox/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/launcher_icon_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/Theme.aTox.DayNight">
<service android:name=".ToxService" android:exported="false"/>

<receiver android:name=".BootReceiver" android:enabled="false" android:exported="false">
Expand Down
12 changes: 12 additions & 0 deletions atox/src/main/kotlin/di/ViewModelModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import ltd.evilcorp.atox.ui.chat.ChatViewModel
import ltd.evilcorp.atox.ui.contact_profile.ContactProfileViewModel
import ltd.evilcorp.atox.ui.contactlist.ContactListViewModel
import ltd.evilcorp.atox.ui.create_profile.CreateProfileViewModel
import ltd.evilcorp.atox.ui.edit_text_value_dialog.EditTextValueDialogViewModel
import ltd.evilcorp.atox.ui.edit_user_profile.EditUserProfileViewModel
import ltd.evilcorp.atox.ui.friend_request.FriendRequestViewModel
import ltd.evilcorp.atox.ui.settings.SettingsViewModel
import ltd.evilcorp.atox.ui.user_profile.UserProfileViewModel
Expand Down Expand Up @@ -77,4 +79,14 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(UserProfileViewModel::class)
abstract fun bindUserProfileViewModel(vm: UserProfileViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(EditUserProfileViewModel::class)
abstract fun bindEditUserProfileViewModel(vm: EditUserProfileViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(EditTextValueDialogViewModel::class)
abstract fun bindEditTextValueDialogViewModel(vm: EditTextValueDialogViewModel): ViewModel
}
106 changes: 106 additions & 0 deletions atox/src/main/kotlin/ui/AvatarMaker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2019-2021 aTox contributors
//
// SPDX-License-Identifier: GPL-3.0-only

package ltd.evilcorp.atox.ui

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.net.Uri
import android.widget.ImageView
import ltd.evilcorp.atox.R
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.User
import kotlin.math.abs

internal enum class SizeUnit {
DP,
PX,
}

/**
* Class for creating an avatar for user or contact and setting it in the ImageView
*/
internal class AvatarMaker {

companion object {
private const val DEFAULT_AVATAR_SIZE_DP = 50
}

private var name: String = ""
private var publicKey: String = ""
private var avatarUri: String = ""
private var initials: String = ""

constructor(contact: Contact) {
name = contact.name
publicKey = contact.publicKey
avatarUri = contact.avatarUri
initials = getInitials()
}
constructor(user: User) {
name = user.name
publicKey = user.publicKey
avatarUri = user.avatarUri
initials = getInitials()
}

/**
* Method will get the initial characters of the name
* @return The initial characters of the name.
*/
private fun getInitials(): String {
val segments = name.split(" ")
if (segments.size == 1) return segments.first().take(1)
return segments.first().take(1) + segments[1][0]
}

/**
* Method will set an avatar to an image view. If avatar image exists then it will be set to the image view,
* otherwise a new avatar image will be created based on the initials of the name
* and the public key for the background color.
* @param imageView The image view for whom to set the avatar image.
* @param size The size of the avatar image in the units specified in sizeUnit (default: DP units).
* @param sizeUnit The size unit of size parameter.
*/
fun setAvatar(imageView: ImageView, size: Int = DEFAULT_AVATAR_SIZE_DP, sizeUnit: SizeUnit = SizeUnit.DP) =
if (avatarUri.isNotEmpty()) {
imageView.setImageURI(Uri.parse(avatarUri))
} else {
val side: Int
val textScale: Float

if (sizeUnit == SizeUnit.DP) {
side = (size * imageView.resources.displayMetrics.density).toInt()
textScale = size.toFloat() / DEFAULT_AVATAR_SIZE_DP
} else {
side = size
textScale = size.toFloat() / dpToPx(DEFAULT_AVATAR_SIZE_DP.toFloat(), imageView.resources)
}

val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
val colors = imageView.resources.getIntArray(R.array.contactBackgrounds)
val backgroundPaint = Paint().apply { color = colors[abs(publicKey.hashCode()).rem(colors.size)] }

val textPaint = Paint().apply {
color = Color.WHITE
textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale
textAlign = Paint.Align.CENTER
isAntiAlias = true
typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
}

val textBounds = Rect()
textPaint.getTextBounds(initials, 0, initials.length, textBounds)
canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint)
canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint)
imageView.setImageBitmap(bitmap)
}
}
2 changes: 1 addition & 1 deletion atox/src/main/kotlin/ui/NotificationHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class NotificationHelper @Inject constructor(
.addAction(
NotificationCompat.Action
.Builder(
IconCompat.createWithResource(context, R.drawable.send),
IconCompat.createWithResource(context, R.drawable.ic_send),
context.getString(R.string.reply),
PendingIntentCompat.getBroadcast(
context,
Expand Down
65 changes: 0 additions & 65 deletions atox/src/main/kotlin/ui/StatusDialog.kt

This file was deleted.

130 changes: 80 additions & 50 deletions atox/src/main/kotlin/ui/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,96 @@

package ltd.evilcorp.atox.ui

import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.net.Uri
import android.widget.ImageView
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.os.Build
import android.util.TypedValue
import android.widget.ImageButton
import androidx.core.content.res.ResourcesCompat
import kotlin.math.abs
import ltd.evilcorp.atox.R
import ltd.evilcorp.core.vo.ConnectionStatus
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.UserStatus

internal fun colorByStatus(resources: Resources, contact: Contact): Int {
if (contact.connectionStatus == ConnectionStatus.None) return ResourcesCompat.getColor(
resources,
R.color.statusOffline,
null
)
return when (contact.status) {
UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null)
UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null)
UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null)
}
}
/**
* Function will return the color of the status of the input contact
* @param resources The resources of the app
* @param contact The contact for whom to retrieve the status color.
* @return The color int.
*/
internal fun colorByContactStatus(resources: Resources, contact: Contact) =
if (contact.connectionStatus == ConnectionStatus.None)
ResourcesCompat.getColor(
resources,
R.color.statusOffline,
null
)
else colorFromStatus(resources, contact.status)

private fun getInitials(contact: Contact): String {
val segments = contact.name.split(" ")
if (segments.size == 1) return segments.first().take(1)
return segments.first().take(1) + segments[1][0]
/**
* Function will return the color of the status of the input user status
* @param resources The resources of the app
* @param status The user status for whom to return the corresponding color.
* @return The color int.
*/
internal fun colorFromStatus(resources: Resources, status: UserStatus) = when (status) {
UserStatus.None -> ResourcesCompat.getColor(resources, R.color.statusAvailable, null)
UserStatus.Away -> ResourcesCompat.getColor(resources, R.color.statusAway, null)
UserStatus.Busy -> ResourcesCompat.getColor(resources, R.color.statusBusy, null)
}

private const val DEFAULT_AVATAR_SIZE_DP = 50
internal fun setAvatarFromContact(imageView: ImageView, contact: Contact, sizeDp: Int = DEFAULT_AVATAR_SIZE_DP) =
if (contact.avatarUri.isNotEmpty()) {
imageView.setImageURI(Uri.parse(contact.avatarUri))
} else {
val side = (sizeDp * imageView.resources.displayMetrics.density).toInt()
val bitmap = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
val colors = imageView.resources.getIntArray(R.array.contactBackgrounds)
val backgroundPaint = Paint().apply { color = colors[abs(contact.publicKey.hashCode()).rem(colors.size)] }
/**
* Function will convert dp (Density Pixels) units to px (Pixels) units
* @param dp The dp units.
* @return The px units as Int.
*/
internal fun dpToPx(dp: Float, res: Resources): Int =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.displayMetrics).toInt()

/**
* Function will return whether or not night mode is set.
* @param context The related context.
* @return Boolean indicating whether or not night mode is set.
*/
internal fun isNightMode(context: Context) = context.resources.configuration.uiMode
.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES

/**
* Function will set a transparent background to an image button with a ripple with color according
* to whether or not night mode is set.
* @param context The related context.
* @param imageButton The ImageButton for whom to set the transparent background with ripple.
*/
internal fun setImageButtonRippleDayNight(context: Context, imageButton: ImageButton) =
if (isNightMode(context))
setImageButtonRipple(imageButton, Color.argb(51, 255, 255, 255))
else setImageButtonRipple(imageButton, Color.argb(31, 0, 0, 0))

val textScale = sizeDp.toFloat() / DEFAULT_AVATAR_SIZE_DP
val textPaint = Paint().apply {
color = Color.WHITE
textSize = imageView.resources.getDimension(R.dimen.contact_avatar_placeholder_text) * textScale
textAlign = Paint.Align.CENTER
isAntiAlias = true
typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
}
val initials = getInitials(contact)
val textBounds = Rect()
textPaint.getTextBounds(initials, 0, initials.length, textBounds)
canvas.drawRoundRect(rect, rect.bottom, rect.right, backgroundPaint)
canvas.drawText(initials, rect.centerX(), rect.centerY() - textBounds.exactCenterY(), textPaint)
imageView.setImageBitmap(bitmap)
/**
* Function will set a transparent background to an image button with a ripple with the input color.
* @param imageButton The ImageButton for whom to set the transparent background with ripple.
* @param colorInt The color of the ripple.
*/
internal fun setImageButtonRipple(imageButton: ImageButton, colorInt: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val background = GradientDrawable()
background.shape = GradientDrawable.OVAL
background.setColor(0x0FFFFFF)

background.cornerRadius = 10f

val mask = GradientDrawable()
mask.shape = GradientDrawable.OVAL
mask.setColor(-0x1000000)
mask.cornerRadius = 5f

val rippleColorLst = ColorStateList.valueOf(colorInt)
val ripple = RippleDrawable(rippleColorLst, background, mask)
imageButton.background = ripple
}
}
2 changes: 1 addition & 1 deletion atox/src/main/kotlin/ui/addcontact/AddContactFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class AddContactFragment : BaseFragment<FragmentAddContactBinding>(FragmentAddCo
contacts = it
}

toolbar.setNavigationIcon(R.drawable.back)
toolbar.setNavigationIcon(R.drawable.ic_back_white)
toolbar.setNavigationOnClickListener {
WindowInsetsControllerCompat(requireActivity().window, view)
.hide(WindowInsetsCompat.Type.ime())
Expand Down
Loading