Skip to content

Commit

Permalink
Merge pull request #1984 from pantasystem/feature/#1939/haptic-feedback
Browse files Browse the repository at this point in the history
感覚フィードバックを実装した
  • Loading branch information
pantasystem committed Nov 22, 2023
2 parents 194d675 + 0ea21bb commit f19e52f
Show file tree
Hide file tree
Showing 20 changed files with 280 additions and 50 deletions.
1 change: 1 addition & 0 deletions modules/common_android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.pantasystem.milktea.common_android.ui.haptic

import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.HapticFeedbackConstants
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.BindingAdapter

object HapticFeedbackController {

fun performClickHapticFeedback(view: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
}
}

fun performLongClickHapticFeedback(view: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
}

fun performToggledHapticFeedback(view: View, isChecked: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (isChecked) {
view.performHapticFeedback(HapticFeedbackConstants.TOGGLE_OFF)
} else {
view.performHapticFeedback(HapticFeedbackConstants.TOGGLE_ON)
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
}
}

@Suppress("DEPRECATION")
fun performTickVibrateHapticFeedback(context: Context) {
val vibratorManager =
context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK).let {
vibratorManager.vibrate(it)
}
}
}


@BindingAdapter("onClickAndPerformHapticFeedback")
@JvmStatic
fun View.onClickAndPerformHapticFeedback(onClick: View.OnClickListener) {
setOnClickListener {
performClickHapticFeedback(it)
onClick.onClick(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.databinding.BindingAdapter
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.internal.managers.FragmentComponentManager
import net.pantasystem.milktea.common_android.ui.Activities
import net.pantasystem.milktea.common_android.ui.haptic.HapticFeedbackController
import net.pantasystem.milktea.common_android.ui.putActivity
import net.pantasystem.milktea.common_navigation.UserDetailNavigationArgs
import net.pantasystem.milktea.model.user.User
Expand All @@ -18,6 +19,7 @@ object UserTransitionHelper {
fun View.showUserDetail(user: User?){
user?: return
this.setOnClickListener { view ->
HapticFeedbackController.performClickHapticFeedback(view)
val context = view.context


Expand Down
3 changes: 2 additions & 1 deletion modules/common_compose/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.VIBRATE" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.pantasystem.milktea.common_compose.haptic

import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

interface HapticFeedback {
fun performClickHapticFeedback()
fun performLongClickHapticFeedback()
fun performTickVibrateHapticFeedback()
}

@Composable
fun rememberHapticFeedback(): HapticFeedback {
val context = LocalContext.current
return remember {
object : HapticFeedback {
override fun performClickHapticFeedback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK).let {
getVibrator().vibrate(it)
}
}
}

override fun performLongClickHapticFeedback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK).let {
getVibrator().vibrate(it)
}
}
}


override fun performTickVibrateHapticFeedback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK).let {
getVibrator().vibrate(it)
}
}

}

@Suppress("DEPRECATION")
private fun getVibrator() = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
}
}
1 change: 1 addition & 0 deletions modules/features/note/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import net.pantasystem.milktea.common_android.ui.putActivity
import net.pantasystem.milktea.common_android.ui.text.CustomEmojiTokenizer
import net.pantasystem.milktea.common_android_ui.account.viewmodel.AccountViewModel
import net.pantasystem.milktea.common_compose.MilkteaStyleConfigApplyAndTheme
import net.pantasystem.milktea.common_compose.haptic.rememberHapticFeedback
import net.pantasystem.milktea.common_navigation.*
import net.pantasystem.milktea.model.channel.Channel
import net.pantasystem.milktea.model.drive.FileProperty
Expand Down Expand Up @@ -224,14 +225,16 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
bindViewModels()

val toolbarBase = getToolbarBase()
val alarmManager: AlarmManager = requireContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager
val alarmManager: AlarmManager =
requireContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager

toolbarBase.apply {
setContent {
MilkteaStyleConfigApplyAndTheme(configRepository = configRepository) {
val currentUser by noteEditorViewModel.user.collectAsState()
val uiState by noteEditorViewModel.uiState.collectAsState()
val isPostAvailable by noteEditorViewModel.isPostAvailable.collectAsState()
val feedback = rememberHapticFeedback()
NoteEditorToolbar(
currentUser = currentUser,
visibility = uiState.sendToState.visibility,
Expand All @@ -240,19 +243,23 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
it.codePointCount(0, it.length)
} ?: 0,
onNavigateUpButtonClicked = {
feedback.performClickHapticFeedback()
finishOrConfirmSaveAsDraftOrDelete()
},
onAvatarIconClicked = {
feedback.performClickHapticFeedback()
accountViewModel.showSwitchDialog()
},
onVisibilityButtonClicked = {
feedback.performClickHapticFeedback()
val dialog = VisibilitySelectionDialogV2()
dialog.show(
childFragmentManager,
VisibilitySelectionDialogV2.FRAGMENT_TAG
)
},
onScheduleButtonClicked = {
feedback.performClickHapticFeedback()
if (uiState.sendToState.schedulePostAt == null) {
// check alarm permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Expand All @@ -261,7 +268,11 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
.setTitle(R.string.alarm_permission_description_title)
.setMessage(R.string.alarm_permission_description_message)
.setPositiveButton(android.R.string.ok) { _, _ ->
startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
startActivity(
Intent(
ACTION_REQUEST_SCHEDULE_EXACT_ALARM
)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing
Expand All @@ -274,6 +285,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
noteEditorViewModel.toggleReservationAt()
},
onPostButtonClicked = {
feedback.performClickHapticFeedback()
noteEditorViewModel.post()
},
)
Expand Down Expand Up @@ -340,21 +352,29 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
binding.filePreview.apply {
setContent {
MilkteaStyleConfigApplyAndTheme(configRepository = configRepository) {
NoteFilePreview(noteEditorViewModel = noteEditorViewModel, onShow = {
val intent = mediaNavigation.newIntent(
MediaNavigationArgs.AFile(
it
val feedback = rememberHapticFeedback()
NoteFilePreview(
noteEditorViewModel = noteEditorViewModel,
onShow = {
feedback.performClickHapticFeedback()
val intent = mediaNavigation.newIntent(
MediaNavigationArgs.AFile(
it
)
)
)

requireActivity().startActivity(intent)
}, onEditFileCaptionSelectionClicked = {
EditFileCaptionDialog.newInstance(it.file, it.comment ?: "")
.show(childFragmentManager, EditFileCaptionDialog.FRAGMENT_TAG)
}, onEditFileNameSelectionClicked = {
EditFileNameDialog.newInstance(it.file, it.name)
.show(childFragmentManager, EditFileNameDialog.FRAGMENT_TAG)
})
requireActivity().startActivity(intent)
},
onEditFileCaptionSelectionClicked = {
feedback.performClickHapticFeedback()
EditFileCaptionDialog.newInstance(it.file, it.comment ?: "")
.show(childFragmentManager, EditFileCaptionDialog.FRAGMENT_TAG)
},
onEditFileNameSelectionClicked = {
feedback.performClickHapticFeedback()
EditFileNameDialog.newInstance(it.file, it.name)
.show(childFragmentManager, EditFileNameDialog.FRAGMENT_TAG)
},
)
}

}
Expand All @@ -363,34 +383,43 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
MilkteaStyleConfigApplyAndTheme(configRepository = configRepository) {
val state by noteEditorViewModel.enableFeatures.collectAsState()
val uiState by noteEditorViewModel.uiState.collectAsState()
val feedback = rememberHapticFeedback()
NoteEditorUserActionMenuLayout(iconColor = getColor(color = R.attr.normalIconTint),
isEnableDrive = state.contains(FeatureType.Drive),
isCw = uiState.formState.hasCw,
isPoll = uiState.poll != null,
onPickFileFromDriveButtonClicked = {
feedback.performClickHapticFeedback()
showDriveFileSelector()
},
onPickFileFromLocalButtonCLicked = {
feedback.performClickHapticFeedback()
showFileManager()
},
onPickImageFromLocalButtonClicked = {
feedback.performClickHapticFeedback()
showMultipleImagePicker()
},
onTogglePollButtonClicked = {
feedback.performClickHapticFeedback()
noteEditorViewModel.enablePoll()
},
onSelectMentionUsersButtonClicked = {
feedback.performClickHapticFeedback()
startMentionToSearchAndSelectUser()
},
onSelectEmojiButtonClicked = {
feedback.performClickHapticFeedback()
CustomEmojiPickerDialog.newInstance(
uiState.currentAccount?.accountId
).show(childFragmentManager, CustomEmojiPickerDialog.FRAGMENT_TAG)
},
onToggleCwButtonClicked = {
feedback.performClickHapticFeedback()
noteEditorViewModel.changeCwEnabled()
},
onSelectDraftNoteButtonClicked = {
feedback.performClickHapticFeedback()
pickDraftNoteActivityResult.launch(
Intent(
requireActivity(),
Expand Down Expand Up @@ -744,7 +773,10 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti
private fun finishOrConfirmSaveAsDraftOrDelete() {
if (noteEditorViewModel.canSaveDraft()) {
if (childFragmentManager.findFragmentByTag(ConfirmSaveAsDraftDialog.FRAGMENT_TAG) == null) {
ConfirmSaveAsDraftDialog().show(childFragmentManager, ConfirmSaveAsDraftDialog.FRAGMENT_TAG)
ConfirmSaveAsDraftDialog().show(
childFragmentManager,
ConfirmSaveAsDraftDialog.FRAGMENT_TAG
)
}
} else {
upTo()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import net.pantasystem.milktea.common.glide.blurhash.BlurHashSource
import net.pantasystem.milktea.common_android.platform.isWifiConnected
import net.pantasystem.milktea.common_android.ui.MediaLayout
import net.pantasystem.milktea.common_android.ui.VisibilityHelper.setMemoVisibility
import net.pantasystem.milktea.common_android.ui.haptic.HapticFeedbackController
import net.pantasystem.milktea.model.setting.Config
import net.pantasystem.milktea.model.setting.DefaultConfig
import net.pantasystem.milktea.note.R
Expand All @@ -43,6 +44,7 @@ object MediaPreviewHelper {
return
}
val listener = View.OnClickListener {
HapticFeedbackController.performClickHapticFeedback(it)
noteCardActionListenerAdapter?.onMediaPreviewClicked(
previewAbleFile = previewAbleFile,
files = previewAbleFileList,
Expand Down Expand Up @@ -185,6 +187,7 @@ object MediaPreviewHelper {
mediaViewData.config,
)
binding.baseFrame.setOnClickListener {
HapticFeedbackController.performClickHapticFeedback(it)
if (previewAbleFile.visibleType == PreviewAbleFile.VisibleType.SensitiveHide) {
noteCardActionListenerAdapter?.onSensitiveMediaPreviewClicked(
mediaViewData,
Expand All @@ -202,9 +205,15 @@ object MediaPreviewHelper {
binding.nsfwMessage.setHideImageMessage(previewAbleFile, mediaViewData.config)
binding.toggleVisibilityButton.setImageResource(if (previewAbleFile.isHiding) R.drawable.ic_baseline_image_24 else R.drawable.ic_baseline_hide_image_24)
binding.toggleVisibilityButton.setOnClickListener {
val visibleState = previewAbleFile.isHidingWithNetworkStateAndConfig(
isMobileNetwork = !isWifiConnected,
mediaDisplayMode = mediaViewData.config?.mediaDisplayMode ?: DefaultConfig.config.mediaDisplayMode
)

// NOTE: ここでのネットワークの状態はbind時のものを使う
// なぜなら表示状態はbindされた時のネットワークの状態を使っているから
mediaViewData.toggleVisibility(index, isMobileNetwork = !isWifiConnected, mediaDisplayMode = mediaViewData.config?.mediaDisplayMode ?: DefaultConfig.config.mediaDisplayMode)
HapticFeedbackController.performToggledHapticFeedback(it, !visibleState)
}

if (existsView == null) {
Expand Down
Loading

0 comments on commit f19e52f

Please sign in to comment.