Feature: Terminal session and extra keys
10
.gitignore
vendored
@ -28,3 +28,13 @@ proguard/
|
|||||||
*.iws
|
*.iws
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
|
||||||
|
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
7
app/CMakeLists.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
|
||||||
|
add_library(neoterm
|
||||||
|
SHARED
|
||||||
|
src/main/cpp/neoterm.cpp)
|
||||||
|
|
43
app/build.gradle
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 25
|
||||||
|
buildToolsVersion "25.0.2"
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "io.neoterm"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 25
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
cppFlags "-std=c++11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path "CMakeLists.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
})
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||||
|
compile project(':chrome-tabs')
|
||||||
|
}
|
25
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /Users/kiva/devel/android-sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,25 @@
|
|||||||
|
package io.neoterm
|
||||||
|
|
||||||
|
import android.support.test.InstrumentationRegistry
|
||||||
|
import android.support.test.runner.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumentation test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see [Testing documentation](http://d.android.com/tools/testing)
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getTargetContext()
|
||||||
|
assertEquals("io.neoterm", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
25
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="io.neoterm">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
222
app/src/main/cpp/neoterm.cpp
Executable file
@ -0,0 +1,222 @@
|
|||||||
|
#include <dirent.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <termios.h>
|
||||||
|
|
||||||
|
#define TERMUX_UNUSED(x) x __attribute__((__unused__))
|
||||||
|
#ifdef __APPLE__
|
||||||
|
# define LACKS_PTSNAME_R
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int throw_runtime_exception(JNIEnv *env, char const *message) {
|
||||||
|
jclass exClass = env->FindClass("java/lang/RuntimeException");
|
||||||
|
env->ThrowNew(exClass, message);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int create_subprocess(JNIEnv *env,
|
||||||
|
char const *cmd,
|
||||||
|
char const *cwd,
|
||||||
|
char *const argv[],
|
||||||
|
char **envp,
|
||||||
|
int *pProcessId,
|
||||||
|
jint rows,
|
||||||
|
jint columns) {
|
||||||
|
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
|
||||||
|
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
|
||||||
|
|
||||||
|
#ifdef LACKS_PTSNAME_R
|
||||||
|
char* devname;
|
||||||
|
#else
|
||||||
|
char devname[64];
|
||||||
|
#endif
|
||||||
|
if (grantpt(ptm) || unlockpt(ptm) ||
|
||||||
|
#ifdef LACKS_PTSNAME_R
|
||||||
|
(devname = ptsname(ptm)) == NULL
|
||||||
|
#else
|
||||||
|
ptsname_r(ptm, devname, sizeof(devname))
|
||||||
|
#endif
|
||||||
|
) {
|
||||||
|
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
|
||||||
|
struct termios tios;
|
||||||
|
tcgetattr(ptm, &tios);
|
||||||
|
tios.c_iflag |= IUTF8;
|
||||||
|
tios.c_iflag &= ~(IXON | IXOFF);
|
||||||
|
tcsetattr(ptm, TCSANOW, &tios);
|
||||||
|
|
||||||
|
/** Set initial winsize. */
|
||||||
|
struct winsize sz = {.ws_row = static_cast<unsigned short>(rows), .ws_col = static_cast<unsigned short>(columns)};
|
||||||
|
ioctl(ptm, TIOCSWINSZ, &sz);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
return throw_runtime_exception(env, "Fork failed");
|
||||||
|
} else if (pid > 0) {
|
||||||
|
*pProcessId = (int) pid;
|
||||||
|
return ptm;
|
||||||
|
} else {
|
||||||
|
// Clear signals which the Android java process may have blocked:
|
||||||
|
sigset_t signals_to_unblock;
|
||||||
|
sigfillset(&signals_to_unblock);
|
||||||
|
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
|
||||||
|
|
||||||
|
close(ptm);
|
||||||
|
setsid();
|
||||||
|
|
||||||
|
int pts = open(devname, O_RDWR);
|
||||||
|
if (pts < 0) exit(-1);
|
||||||
|
|
||||||
|
dup2(pts, 0);
|
||||||
|
dup2(pts, 1);
|
||||||
|
dup2(pts, 2);
|
||||||
|
|
||||||
|
DIR *self_dir = opendir("/proc/self/fd");
|
||||||
|
if (self_dir != NULL) {
|
||||||
|
int self_dir_fd = dirfd(self_dir);
|
||||||
|
struct dirent *entry;
|
||||||
|
while ((entry = readdir(self_dir)) != NULL) {
|
||||||
|
int fd = atoi(entry->d_name);
|
||||||
|
if (fd > 2 && fd != self_dir_fd) close(fd);
|
||||||
|
}
|
||||||
|
closedir(self_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearenv();
|
||||||
|
if (envp) for (; *envp; ++envp) putenv(*envp);
|
||||||
|
|
||||||
|
if (chdir(cwd) != 0) {
|
||||||
|
char *error_message;
|
||||||
|
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
|
||||||
|
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1)
|
||||||
|
error_message =
|
||||||
|
const_cast<char *>("chdir()");
|
||||||
|
perror(error_message);
|
||||||
|
fflush(stderr);
|
||||||
|
}
|
||||||
|
execvp(cmd, argv);
|
||||||
|
// Show terminal output about failing exec() call:
|
||||||
|
char *error_message;
|
||||||
|
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1)
|
||||||
|
const_cast<char *>("exec()");;
|
||||||
|
perror(error_message);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL Java_io_neoterm_terminal_JNI_createSubprocess(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass TERMUX_UNUSED(clazz),
|
||||||
|
jstring cmd,
|
||||||
|
jstring cwd,
|
||||||
|
jobjectArray args,
|
||||||
|
jobjectArray envVars,
|
||||||
|
jintArray processIdArray,
|
||||||
|
jint rows,
|
||||||
|
jint columns) {
|
||||||
|
jsize size = args ? env->GetArrayLength(args) : 0;
|
||||||
|
char **argv = NULL;
|
||||||
|
if (size > 0) {
|
||||||
|
argv = (char **) malloc((size + 1) * sizeof(char *));
|
||||||
|
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
jstring arg_java_string = (jstring) env->GetObjectArrayElement(args, i);
|
||||||
|
char const *arg_utf8 = env->GetStringUTFChars(arg_java_string, NULL);
|
||||||
|
if (!arg_utf8)
|
||||||
|
return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
|
||||||
|
argv[i] = strdup(arg_utf8);
|
||||||
|
env->ReleaseStringUTFChars(arg_java_string, arg_utf8);
|
||||||
|
}
|
||||||
|
argv[size] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = envVars ? env->GetArrayLength(envVars) : 0;
|
||||||
|
char **envp = NULL;
|
||||||
|
if (size > 0) {
|
||||||
|
envp = (char **) malloc((size + 1) * sizeof(char *));
|
||||||
|
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
jstring env_java_string = (jstring) env->GetObjectArrayElement(envVars, i);
|
||||||
|
char const *env_utf8 = env->GetStringUTFChars(env_java_string, 0);
|
||||||
|
if (!env_utf8)
|
||||||
|
return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
|
||||||
|
envp[i] = strdup(env_utf8);
|
||||||
|
env->ReleaseStringUTFChars(env_java_string, env_utf8);
|
||||||
|
}
|
||||||
|
envp[size] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int procId = 0;
|
||||||
|
char const *cmd_cwd = env->GetStringUTFChars(cwd, NULL);
|
||||||
|
char const *cmd_utf8 = env->GetStringUTFChars(cmd, NULL);
|
||||||
|
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
|
||||||
|
env->ReleaseStringUTFChars(cmd, cmd_utf8);
|
||||||
|
env->ReleaseStringUTFChars(cmd, cmd_cwd);
|
||||||
|
|
||||||
|
if (argv) {
|
||||||
|
for (char **tmp = argv; *tmp; ++tmp) free(*tmp);
|
||||||
|
free(argv);
|
||||||
|
}
|
||||||
|
if (envp) {
|
||||||
|
for (char **tmp = envp; *tmp; ++tmp) free(*tmp);
|
||||||
|
free(envp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int *pProcId = (int *) env->GetPrimitiveArrayCritical(processIdArray, NULL);
|
||||||
|
if (!pProcId)
|
||||||
|
return throw_runtime_exception(env,
|
||||||
|
"JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
|
||||||
|
|
||||||
|
*pProcId = procId;
|
||||||
|
env->ReleasePrimitiveArrayCritical(processIdArray, pProcId, 0);
|
||||||
|
|
||||||
|
return ptm;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_io_neoterm_terminal_JNI_setPtyWindowSize(JNIEnv *TERMUX_UNUSED(env),
|
||||||
|
jclass TERMUX_UNUSED(clazz),
|
||||||
|
jint fd, jint rows,
|
||||||
|
jint cols) {
|
||||||
|
struct winsize sz = {.ws_row = static_cast<unsigned short>(rows), .ws_col = static_cast<unsigned short>(cols)};
|
||||||
|
ioctl(fd, TIOCSWINSZ, &sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_io_neoterm_terminal_JNI_setPtyUTF8Mode(JNIEnv *TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz),
|
||||||
|
jint fd) {
|
||||||
|
struct termios tios;
|
||||||
|
tcgetattr(fd, &tios);
|
||||||
|
if ((tios.c_iflag & IUTF8) == 0) {
|
||||||
|
tios.c_iflag |= IUTF8;
|
||||||
|
tcsetattr(fd, TCSANOW, &tios);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT int JNICALL
|
||||||
|
Java_io_neoterm_terminal_JNI_waitFor(JNIEnv *TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz),
|
||||||
|
jint pid) {
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
if (WIFEXITED(status)) {
|
||||||
|
return WEXITSTATUS(status);
|
||||||
|
} else if (WIFSIGNALED(status)) {
|
||||||
|
return -WTERMSIG(status);
|
||||||
|
} else {
|
||||||
|
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_io_neoterm_terminal_JNI_close(JNIEnv *TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz),
|
||||||
|
jint fileDescriptor) {
|
||||||
|
close(fileDescriptor);
|
||||||
|
}
|
131
app/src/main/java/io/neoterm/MainActivity.kt
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package io.neoterm
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.InputDevice
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import io.neoterm.terminal.TerminalSession
|
||||||
|
import io.neoterm.view.ExtraKeysView
|
||||||
|
import io.neoterm.view.TerminalView
|
||||||
|
import io.neoterm.view.TerminalViewClient
|
||||||
|
|
||||||
|
class MainActivity : Activity() {
|
||||||
|
private lateinit var extraKeysView: ExtraKeysView
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
extraKeysView = findViewById(R.id.extra_keys) as ExtraKeysView
|
||||||
|
val view = findViewById(R.id.terminal_view) as TerminalView
|
||||||
|
|
||||||
|
view.setBackgroundColor(Color.BLACK)
|
||||||
|
view.textSize = 30
|
||||||
|
view.setTypeface(Typeface.MONOSPACE)
|
||||||
|
|
||||||
|
val session = TerminalSession("/system/bin/sh", "/",
|
||||||
|
arrayOf("/system/bin/sh"),
|
||||||
|
arrayOf("TERM=screen", "HOME=" + filesDir),
|
||||||
|
object : TerminalSession.SessionChangedCallback {
|
||||||
|
override fun onBell(session: TerminalSession?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClipboardText(session: TerminalSession?, text: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onColorsChanged(session: TerminalSession?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionFinished(finishedSession: TerminalSession?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(changedSession: TerminalSession?) {
|
||||||
|
view.onScreenUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTitleChanged(changedSession: TerminalSession?) {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
view.setOnKeyListener(object : TerminalViewClient {
|
||||||
|
internal var mVirtualControlKeyDown: Boolean = false
|
||||||
|
internal var mVirtualFnKeyDown: Boolean = false
|
||||||
|
|
||||||
|
override fun onScale(scale: Float): Float {
|
||||||
|
if (scale < 0.9f || scale > 1.1f) {
|
||||||
|
val increase = scale > 1f
|
||||||
|
val changedSize = (if (increase) 1 else -1) * 2
|
||||||
|
view.textSize = view.textSize + changedSize
|
||||||
|
return 1.0f
|
||||||
|
}
|
||||||
|
return scale
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSingleTapUp(e: MotionEvent?) {
|
||||||
|
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||||
|
.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldBackButtonBeMappedToEscape(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun copyModeChanged(copyMode: Boolean) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {
|
||||||
|
// TODO
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean {
|
||||||
|
return handleVirtualKeys(keyCode, e, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readControlKey(): Boolean {
|
||||||
|
return extraKeysView.readControlButton() || mVirtualControlKeyDown
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readAltKey(): Boolean {
|
||||||
|
return extraKeysView.readAltButton() || mVirtualFnKeyDown
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession?): Boolean {
|
||||||
|
// TODO
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(event: MotionEvent?): Boolean {
|
||||||
|
// TODO
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleVirtualKeys(keyCode: Int, event: KeyEvent?, down: Boolean): Boolean {
|
||||||
|
if (event == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val inputDevice = event.device
|
||||||
|
if (inputDevice != null && inputDevice.keyboardType == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||||
|
return false
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||||
|
mVirtualControlKeyDown = down
|
||||||
|
return true
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
|
mVirtualFnKeyDown = down
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
view.attachSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
108
app/src/main/java/io/neoterm/terminal/ByteQueue.java
Executable file
@ -0,0 +1,108 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
/** A circular byte buffer allowing one producer and one consumer thread. */
|
||||||
|
final class ByteQueue {
|
||||||
|
|
||||||
|
private final byte[] mBuffer;
|
||||||
|
private int mHead;
|
||||||
|
private int mStoredBytes;
|
||||||
|
private boolean mOpen = true;
|
||||||
|
|
||||||
|
public ByteQueue(int size) {
|
||||||
|
mBuffer = new byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void close() {
|
||||||
|
mOpen = false;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int read(byte[] buffer, boolean block) {
|
||||||
|
while (mStoredBytes == 0 && mOpen) {
|
||||||
|
if (block) {
|
||||||
|
try {
|
||||||
|
wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mOpen) return -1;
|
||||||
|
|
||||||
|
int totalRead = 0;
|
||||||
|
int bufferLength = mBuffer.length;
|
||||||
|
boolean wasFull = bufferLength == mStoredBytes;
|
||||||
|
int length = buffer.length;
|
||||||
|
int offset = 0;
|
||||||
|
while (length > 0 && mStoredBytes > 0) {
|
||||||
|
int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
|
||||||
|
int bytesToCopy = Math.min(length, oneRun);
|
||||||
|
System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
|
||||||
|
mHead += bytesToCopy;
|
||||||
|
if (mHead >= bufferLength) mHead = 0;
|
||||||
|
mStoredBytes -= bytesToCopy;
|
||||||
|
length -= bytesToCopy;
|
||||||
|
offset += bytesToCopy;
|
||||||
|
totalRead += bytesToCopy;
|
||||||
|
}
|
||||||
|
if (wasFull) notify();
|
||||||
|
return totalRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to write the specified portion of the provided buffer to the queue.
|
||||||
|
* <p/>
|
||||||
|
* Returns whether the output was totally written, false if it was closed before.
|
||||||
|
*/
|
||||||
|
public boolean write(byte[] buffer, int offset, int lengthToWrite) {
|
||||||
|
if (lengthToWrite + offset > buffer.length) {
|
||||||
|
throw new IllegalArgumentException("length + offset > buffer.length");
|
||||||
|
} else if (lengthToWrite <= 0) {
|
||||||
|
throw new IllegalArgumentException("length <= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int bufferLength = mBuffer.length;
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
while (lengthToWrite > 0) {
|
||||||
|
while (bufferLength == mStoredBytes && mOpen) {
|
||||||
|
try {
|
||||||
|
wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mOpen) return false;
|
||||||
|
final boolean wasEmpty = mStoredBytes == 0;
|
||||||
|
int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
|
||||||
|
lengthToWrite -= bytesToWriteBeforeWaiting;
|
||||||
|
|
||||||
|
while (bytesToWriteBeforeWaiting > 0) {
|
||||||
|
int tail = mHead + mStoredBytes;
|
||||||
|
int oneRun;
|
||||||
|
if (tail >= bufferLength) {
|
||||||
|
// Buffer: [.............]
|
||||||
|
// ________________H_______T
|
||||||
|
// =>
|
||||||
|
// Buffer: [.............]
|
||||||
|
// ___________T____H
|
||||||
|
// onRun= _____----_
|
||||||
|
tail = tail - bufferLength;
|
||||||
|
oneRun = mHead - tail;
|
||||||
|
} else {
|
||||||
|
oneRun = bufferLength - tail;
|
||||||
|
}
|
||||||
|
int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
|
||||||
|
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
|
||||||
|
offset += bytesToCopy;
|
||||||
|
bytesToWriteBeforeWaiting -= bytesToCopy;
|
||||||
|
mStoredBytes += bytesToCopy;
|
||||||
|
}
|
||||||
|
if (wasEmpty) notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/java/io/neoterm/terminal/EmulatorDebug.java
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public final class EmulatorDebug {
|
||||||
|
|
||||||
|
/** The tag to use with {@link Log}. */
|
||||||
|
public static final String LOG_TAG = "neoterm-termux";
|
||||||
|
|
||||||
|
}
|
41
app/src/main/java/io/neoterm/terminal/JNI.java
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
|
||||||
|
*/
|
||||||
|
final class JNI {
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("neoterm");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
|
||||||
|
* subprocess.
|
||||||
|
* <p/>
|
||||||
|
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
|
||||||
|
*
|
||||||
|
* @param cmd The command to execute
|
||||||
|
* @param cwd The current working directory for the executed command
|
||||||
|
* @param args An array of arguments to the command
|
||||||
|
* @param envVars An array of strings of the form "VAR=value" to be added to the environment of the process
|
||||||
|
* @param processId A one-element array to which the process ID of the started process will be written.
|
||||||
|
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
|
||||||
|
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
|
||||||
|
*/
|
||||||
|
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
|
||||||
|
|
||||||
|
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
|
||||||
|
public static native void setPtyWindowSize(int fd, int rows, int cols);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
|
||||||
|
*
|
||||||
|
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
|
||||||
|
*/
|
||||||
|
public static native int waitFor(int processId);
|
||||||
|
|
||||||
|
/** Close a file descriptor through the close(2) system call. */
|
||||||
|
public static native void close(int fileDescriptor);
|
||||||
|
|
||||||
|
}
|
313
app/src/main/java/io/neoterm/terminal/KeyHandler.java
Executable file
@ -0,0 +1,313 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static android.view.KeyEvent.KEYCODE_BACK;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_BREAK;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_DEL;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_ESCAPE;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F1;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F10;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F11;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F12;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F2;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F3;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F4;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F5;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F6;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F7;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F8;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_F9;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_INSERT;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_MOVE_END;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_MOVE_HOME;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_3;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_4;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_5;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_6;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_7;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_8;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_9;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_ADD;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_COMMA;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_DIVIDE;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_DOT;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_ENTER;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_EQUALS;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_MULTIPLY;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_SUBTRACT;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_NUM_LOCK;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_PAGE_DOWN;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_PAGE_UP;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_SPACE;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_SYSRQ;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_TAB;
|
||||||
|
|
||||||
|
public final class KeyHandler {
|
||||||
|
|
||||||
|
public static final int KEYMOD_ALT = 0x80000000;
|
||||||
|
public static final int KEYMOD_CTRL = 0x40000000;
|
||||||
|
public static final int KEYMOD_SHIFT = 0x20000000;
|
||||||
|
|
||||||
|
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
|
||||||
|
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
|
||||||
|
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
|
||||||
|
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_MOVE_HOME); // Shifted home
|
||||||
|
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
|
||||||
|
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
|
||||||
|
|
||||||
|
TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
|
||||||
|
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
|
||||||
|
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
|
||||||
|
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
|
||||||
|
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
|
||||||
|
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
|
||||||
|
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
|
||||||
|
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
|
||||||
|
|
||||||
|
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
|
||||||
|
|
||||||
|
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
|
||||||
|
TERMCAP_TO_KEYCODE.put("kh", KEYCODE_MOVE_HOME);
|
||||||
|
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
|
||||||
|
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
|
||||||
|
|
||||||
|
// K1=Upper left of keypad:
|
||||||
|
// t_K1 <kHome> keypad home key
|
||||||
|
// t_K3 <kPageUp> keypad page-up key
|
||||||
|
// t_K4 <kEnd> keypad end key
|
||||||
|
// t_K5 <kPageDown> keypad page-down key
|
||||||
|
TERMCAP_TO_KEYCODE.put("K1", KEYCODE_MOVE_HOME);
|
||||||
|
TERMCAP_TO_KEYCODE.put("K3", KEYCODE_PAGE_UP);
|
||||||
|
TERMCAP_TO_KEYCODE.put("K4", KEYCODE_MOVE_END);
|
||||||
|
TERMCAP_TO_KEYCODE.put("K5", KEYCODE_PAGE_DOWN);
|
||||||
|
|
||||||
|
TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
|
||||||
|
|
||||||
|
TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab_switcher
|
||||||
|
TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
|
||||||
|
TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
|
||||||
|
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
|
||||||
|
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
|
||||||
|
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
|
||||||
|
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
|
||||||
|
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
|
||||||
|
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
|
||||||
|
|
||||||
|
TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END);
|
||||||
|
TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) {
|
||||||
|
Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap);
|
||||||
|
if (keyCodeAndMod == null) return null;
|
||||||
|
int keyCode = keyCodeAndMod;
|
||||||
|
int keyMod = 0;
|
||||||
|
if ((keyCode & KEYMOD_SHIFT) != 0) {
|
||||||
|
keyMod |= KEYMOD_SHIFT;
|
||||||
|
keyCode &= ~KEYMOD_SHIFT;
|
||||||
|
}
|
||||||
|
if ((keyCode & KEYMOD_CTRL) != 0) {
|
||||||
|
keyMod |= KEYMOD_CTRL;
|
||||||
|
keyCode &= ~KEYMOD_CTRL;
|
||||||
|
}
|
||||||
|
if ((keyCode & KEYMOD_ALT) != 0) {
|
||||||
|
keyMod |= KEYMOD_ALT;
|
||||||
|
keyCode &= ~KEYMOD_ALT;
|
||||||
|
}
|
||||||
|
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case KEYCODE_DPAD_CENTER:
|
||||||
|
return "\015";
|
||||||
|
|
||||||
|
case KEYCODE_DPAD_UP:
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
|
||||||
|
case KEYCODE_DPAD_DOWN:
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
|
||||||
|
case KEYCODE_DPAD_RIGHT:
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
|
||||||
|
case KEYCODE_DPAD_LEFT:
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
|
||||||
|
|
||||||
|
case KEYCODE_MOVE_HOME:
|
||||||
|
// Note that KEYCODE_HOME is handled by the system and never delivered to applications.
|
||||||
|
// On a Logitech k810 keyboard KEYCODE_MOVE_HOME is sent by FN+LeftArrow.
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
|
||||||
|
case KEYCODE_MOVE_END:
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
|
||||||
|
|
||||||
|
// An xterm can send function keys F1 to F4 in two modes: vt100 compatible or
|
||||||
|
// not. Because Vim may not know what the xterm is sending, both types of keys
|
||||||
|
// are recognized. The same happens for the <Home> and <End> keys.
|
||||||
|
// normal vt100 ~
|
||||||
|
// <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm*
|
||||||
|
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm*
|
||||||
|
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
|
||||||
|
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
|
||||||
|
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
|
||||||
|
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
|
||||||
|
case KEYCODE_F1:
|
||||||
|
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
|
||||||
|
case KEYCODE_F2:
|
||||||
|
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
|
||||||
|
case KEYCODE_F3:
|
||||||
|
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
|
||||||
|
case KEYCODE_F4:
|
||||||
|
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
|
||||||
|
case KEYCODE_F5:
|
||||||
|
return transformForModifiers("\033[15", keyMode, '~');
|
||||||
|
case KEYCODE_F6:
|
||||||
|
return transformForModifiers("\033[17", keyMode, '~');
|
||||||
|
case KEYCODE_F7:
|
||||||
|
return transformForModifiers("\033[18", keyMode, '~');
|
||||||
|
case KEYCODE_F8:
|
||||||
|
return transformForModifiers("\033[19", keyMode, '~');
|
||||||
|
case KEYCODE_F9:
|
||||||
|
return transformForModifiers("\033[20", keyMode, '~');
|
||||||
|
case KEYCODE_F10:
|
||||||
|
return transformForModifiers("\033[21", keyMode, '~');
|
||||||
|
case KEYCODE_F11:
|
||||||
|
return transformForModifiers("\033[23", keyMode, '~');
|
||||||
|
case KEYCODE_F12:
|
||||||
|
return transformForModifiers("\033[24", keyMode, '~');
|
||||||
|
|
||||||
|
case KEYCODE_SYSRQ:
|
||||||
|
return "\033[32~"; // Sys Request / Print
|
||||||
|
// Is this Scroll lock? case Cancel: return "\033[33~";
|
||||||
|
case KEYCODE_BREAK:
|
||||||
|
return "\033[34~"; // Pause/Break
|
||||||
|
|
||||||
|
case KEYCODE_ESCAPE:
|
||||||
|
case KEYCODE_BACK:
|
||||||
|
return "\033";
|
||||||
|
|
||||||
|
case KEYCODE_INSERT:
|
||||||
|
return transformForModifiers("\033[2", keyMode, '~');
|
||||||
|
case KEYCODE_FORWARD_DEL:
|
||||||
|
return transformForModifiers("\033[3", keyMode, '~');
|
||||||
|
|
||||||
|
case KEYCODE_PAGE_UP:
|
||||||
|
return "\033[5~";
|
||||||
|
case KEYCODE_PAGE_DOWN:
|
||||||
|
return "\033[6~";
|
||||||
|
case KEYCODE_DEL:
|
||||||
|
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
||||||
|
// Just do what xterm and gnome-terminal does:
|
||||||
|
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
|
||||||
|
case KEYCODE_NUM_LOCK:
|
||||||
|
return "\033OP";
|
||||||
|
|
||||||
|
case KEYCODE_SPACE:
|
||||||
|
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
|
||||||
|
// combining accent to be written):
|
||||||
|
return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0";
|
||||||
|
case KEYCODE_TAB:
|
||||||
|
// This is back-tab_switcher when shifted:
|
||||||
|
return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z";
|
||||||
|
case KEYCODE_ENTER:
|
||||||
|
return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r";
|
||||||
|
|
||||||
|
case KEYCODE_NUMPAD_ENTER:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n";
|
||||||
|
case KEYCODE_NUMPAD_MULTIPLY:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*";
|
||||||
|
case KEYCODE_NUMPAD_ADD:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
|
||||||
|
case KEYCODE_NUMPAD_COMMA:
|
||||||
|
return ",";
|
||||||
|
case KEYCODE_NUMPAD_DOT:
|
||||||
|
return keypadApplication ? "\033On" : ".";
|
||||||
|
case KEYCODE_NUMPAD_SUBTRACT:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
||||||
|
case KEYCODE_NUMPAD_DIVIDE:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
||||||
|
case KEYCODE_NUMPAD_0:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
||||||
|
case KEYCODE_NUMPAD_1:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
||||||
|
case KEYCODE_NUMPAD_2:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
|
||||||
|
case KEYCODE_NUMPAD_3:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
|
||||||
|
case KEYCODE_NUMPAD_4:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
|
||||||
|
case KEYCODE_NUMPAD_5:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
|
||||||
|
case KEYCODE_NUMPAD_6:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
|
||||||
|
case KEYCODE_NUMPAD_7:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
|
||||||
|
case KEYCODE_NUMPAD_8:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
|
||||||
|
case KEYCODE_NUMPAD_9:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
|
||||||
|
case KEYCODE_NUMPAD_EQUALS:
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String transformForModifiers(String start, int keymod, char lastChar) {
|
||||||
|
int modifier;
|
||||||
|
switch (keymod) {
|
||||||
|
case KEYMOD_SHIFT:
|
||||||
|
modifier = 2;
|
||||||
|
break;
|
||||||
|
case KEYMOD_ALT:
|
||||||
|
modifier = 3;
|
||||||
|
break;
|
||||||
|
case (KEYMOD_SHIFT | KEYMOD_ALT):
|
||||||
|
modifier = 4;
|
||||||
|
break;
|
||||||
|
case KEYMOD_CTRL:
|
||||||
|
modifier = 5;
|
||||||
|
break;
|
||||||
|
case KEYMOD_SHIFT | KEYMOD_CTRL:
|
||||||
|
modifier = 6;
|
||||||
|
break;
|
||||||
|
case KEYMOD_ALT | KEYMOD_CTRL:
|
||||||
|
modifier = 7;
|
||||||
|
break;
|
||||||
|
case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL:
|
||||||
|
modifier = 8;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return start + lastChar;
|
||||||
|
}
|
||||||
|
return start + (";" + modifier) + lastChar;
|
||||||
|
}
|
||||||
|
}
|
425
app/src/main/java/io/neoterm/terminal/TerminalBuffer.java
Executable file
@ -0,0 +1,425 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
||||||
|
* history.
|
||||||
|
* <p>
|
||||||
|
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
|
||||||
|
*/
|
||||||
|
public final class TerminalBuffer {
|
||||||
|
|
||||||
|
TerminalRow[] mLines;
|
||||||
|
/** The length of {@link #mLines}. */
|
||||||
|
int mTotalRows;
|
||||||
|
/** The number of rows and columns visible on the screen. */
|
||||||
|
int mScreenRows, mColumns;
|
||||||
|
/** The number of rows kept in history. */
|
||||||
|
private int mActiveTranscriptRows = 0;
|
||||||
|
/** The index in the circular buffer where the visible screen starts. */
|
||||||
|
private int mScreenFirstRow = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a transcript screen.
|
||||||
|
*
|
||||||
|
* @param columns the width of the screen in characters.
|
||||||
|
* @param totalRows the height of the entire text area, in rows of text.
|
||||||
|
* @param screenRows the height of just the screen, not including the transcript that holds lines that have scrolled off
|
||||||
|
* the top of the screen.
|
||||||
|
*/
|
||||||
|
public TerminalBuffer(int columns, int totalRows, int screenRows) {
|
||||||
|
mColumns = columns;
|
||||||
|
mTotalRows = totalRows;
|
||||||
|
mScreenRows = screenRows;
|
||||||
|
mLines = new TerminalRow[totalRows];
|
||||||
|
|
||||||
|
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTranscriptText() {
|
||||||
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
final int columns = mColumns;
|
||||||
|
|
||||||
|
if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows();
|
||||||
|
if (selY2 >= mScreenRows) selY2 = mScreenRows - 1;
|
||||||
|
|
||||||
|
for (int row = selY1; row <= selY2; row++) {
|
||||||
|
int x1 = (row == selY1) ? selX1 : 0;
|
||||||
|
int x2;
|
||||||
|
if (row == selY2) {
|
||||||
|
x2 = selX2 + 1;
|
||||||
|
if (x2 > columns) x2 = columns;
|
||||||
|
} else {
|
||||||
|
x2 = columns;
|
||||||
|
}
|
||||||
|
TerminalRow lineObject = mLines[externalToInternalRow(row)];
|
||||||
|
int x1Index = lineObject.findStartOfColumn(x1);
|
||||||
|
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
|
||||||
|
if (x2Index == x1Index) {
|
||||||
|
// Selected the start of a wide character.
|
||||||
|
x2Index = lineObject.findStartOfColumn(x2 + 1);
|
||||||
|
}
|
||||||
|
char[] line = lineObject.mText;
|
||||||
|
int lastPrintingCharIndex = -1;
|
||||||
|
int i;
|
||||||
|
boolean rowLineWrap = getLineWrap(row);
|
||||||
|
if (rowLineWrap && x2 == columns) {
|
||||||
|
// If the line was wrapped, we shouldn't lose trailing space:
|
||||||
|
lastPrintingCharIndex = x2Index - 1;
|
||||||
|
} else {
|
||||||
|
for (i = x1Index; i < x2Index; ++i) {
|
||||||
|
char c = line[i];
|
||||||
|
if (c != ' ') lastPrintingCharIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastPrintingCharIndex != -1)
|
||||||
|
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||||
|
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveTranscriptRows() {
|
||||||
|
return mActiveTranscriptRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActiveRows() {
|
||||||
|
return mActiveTranscriptRows + mScreenRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* - External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
|
||||||
|
* - Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
|
||||||
|
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
|
||||||
|
*
|
||||||
|
* External ↔ Internal:
|
||||||
|
*
|
||||||
|
* [ ... ] [ ... ]
|
||||||
|
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
|
||||||
|
* [ ... ] [ ... ]
|
||||||
|
* [ 0 (visible screen starts here) ] ↔ [ mScreenFirstRow ]
|
||||||
|
* [ ... ] [ ... ]
|
||||||
|
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param externalRow a row in the external coordinate system.
|
||||||
|
* @return The row corresponding to the input argument in the private coordinate system.
|
||||||
|
*/
|
||||||
|
public int externalToInternalRow(int externalRow) {
|
||||||
|
if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
|
||||||
|
throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows);
|
||||||
|
final int internalRow = mScreenFirstRow + externalRow;
|
||||||
|
return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLineWrap(int row) {
|
||||||
|
mLines[externalToInternalRow(row)].mLineWrap = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getLineWrap(int row) {
|
||||||
|
return mLines[externalToInternalRow(row)].mLineWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearLineWrap(int row) {
|
||||||
|
mLines[externalToInternalRow(row)].mLineWrap = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||||
|
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||||
|
*
|
||||||
|
* @param newColumns The number of columns the screen should have.
|
||||||
|
* @param newRows The number of rows the screen should have.
|
||||||
|
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||||
|
*/
|
||||||
|
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) {
|
||||||
|
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
|
||||||
|
if (newColumns == mColumns && newRows <= mTotalRows) {
|
||||||
|
// Fast resize where just the rows changed.
|
||||||
|
int shiftDownOfTopRow = mScreenRows - newRows;
|
||||||
|
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
|
||||||
|
// Shrinking. Check if we can skip blank rows at bottom below cursor.
|
||||||
|
for (int i = mScreenRows - 1; i > 0; i--) {
|
||||||
|
if (cursor[1] >= i) break;
|
||||||
|
int r = externalToInternalRow(i);
|
||||||
|
if (mLines[r] == null || mLines[r].isBlank()) {
|
||||||
|
if (--shiftDownOfTopRow == 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (shiftDownOfTopRow < 0) {
|
||||||
|
// Negative shift down = expanding. Only move screen up if there is transcript to show:
|
||||||
|
int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows);
|
||||||
|
if (shiftDownOfTopRow != actualShift) {
|
||||||
|
// The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
|
||||||
|
for (int i = 0; i < actualShift - shiftDownOfTopRow; i++)
|
||||||
|
allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle);
|
||||||
|
shiftDownOfTopRow = actualShift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mScreenFirstRow += shiftDownOfTopRow;
|
||||||
|
mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows);
|
||||||
|
mTotalRows = newTotalRows;
|
||||||
|
mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow);
|
||||||
|
cursor[1] -= shiftDownOfTopRow;
|
||||||
|
mScreenRows = newRows;
|
||||||
|
} else {
|
||||||
|
// Copy away old state and update new:
|
||||||
|
TerminalRow[] oldLines = mLines;
|
||||||
|
mLines = new TerminalRow[newTotalRows];
|
||||||
|
for (int i = 0; i < newTotalRows; i++)
|
||||||
|
mLines[i] = new TerminalRow(newColumns, currentStyle);
|
||||||
|
|
||||||
|
final int oldActiveTranscriptRows = mActiveTranscriptRows;
|
||||||
|
final int oldScreenFirstRow = mScreenFirstRow;
|
||||||
|
final int oldScreenRows = mScreenRows;
|
||||||
|
final int oldTotalRows = mTotalRows;
|
||||||
|
mTotalRows = newTotalRows;
|
||||||
|
mScreenRows = newRows;
|
||||||
|
mActiveTranscriptRows = mScreenFirstRow = 0;
|
||||||
|
mColumns = newColumns;
|
||||||
|
|
||||||
|
int newCursorRow = -1;
|
||||||
|
int newCursorColumn = -1;
|
||||||
|
int oldCursorRow = cursor[1];
|
||||||
|
int oldCursorColumn = cursor[0];
|
||||||
|
boolean newCursorPlaced = false;
|
||||||
|
|
||||||
|
int currentOutputExternalRow = 0;
|
||||||
|
int currentOutputExternalColumn = 0;
|
||||||
|
|
||||||
|
// Loop over every character in the initial state.
|
||||||
|
// Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
|
||||||
|
// keep track how many blank lines we have skipped if we later on find a non-blank line.
|
||||||
|
int skippedBlankLines = 0;
|
||||||
|
for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) {
|
||||||
|
// Do what externalToInternalRow() does but for the old state:
|
||||||
|
int internalOldRow = oldScreenFirstRow + externalOldRow;
|
||||||
|
internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows);
|
||||||
|
|
||||||
|
TerminalRow oldLine = oldLines[internalOldRow];
|
||||||
|
boolean cursorAtThisRow = externalOldRow == oldCursorRow;
|
||||||
|
// The cursor may only be on a non-null line, which we should not skip:
|
||||||
|
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
|
||||||
|
skippedBlankLines++;
|
||||||
|
continue;
|
||||||
|
} else if (skippedBlankLines > 0) {
|
||||||
|
// After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
|
||||||
|
for (int i = 0; i < skippedBlankLines; i++) {
|
||||||
|
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||||
|
scrollDownOneLine(0, mScreenRows, currentStyle);
|
||||||
|
} else {
|
||||||
|
currentOutputExternalRow++;
|
||||||
|
}
|
||||||
|
currentOutputExternalColumn = 0;
|
||||||
|
}
|
||||||
|
skippedBlankLines = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastNonSpaceIndex = 0;
|
||||||
|
boolean justToCursor = false;
|
||||||
|
if (cursorAtThisRow || oldLine.mLineWrap) {
|
||||||
|
// Take the whole line, either because of cursor on it, or if line wrapping.
|
||||||
|
lastNonSpaceIndex = oldLine.getSpaceUsed();
|
||||||
|
if (cursorAtThisRow) justToCursor = true;
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < oldLine.getSpaceUsed(); i++)
|
||||||
|
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
|
||||||
|
if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */)
|
||||||
|
lastNonSpaceIndex = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentOldCol = 0;
|
||||||
|
long styleAtCol = 0;
|
||||||
|
for (int i = 0; i < lastNonSpaceIndex; i++) {
|
||||||
|
// Note that looping over java character, not cells.
|
||||||
|
char c = oldLine.mText[i];
|
||||||
|
int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
|
||||||
|
int displayWidth = WcWidth.width(codePoint);
|
||||||
|
// Use the last style if this is a zero-width character:
|
||||||
|
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol);
|
||||||
|
|
||||||
|
// Line wrap as necessary:
|
||||||
|
if (currentOutputExternalColumn + displayWidth > mColumns) {
|
||||||
|
setLineWrap(currentOutputExternalRow);
|
||||||
|
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||||
|
if (newCursorPlaced) newCursorRow--;
|
||||||
|
scrollDownOneLine(0, mScreenRows, currentStyle);
|
||||||
|
} else {
|
||||||
|
currentOutputExternalRow++;
|
||||||
|
}
|
||||||
|
currentOutputExternalColumn = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0);
|
||||||
|
int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar;
|
||||||
|
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol);
|
||||||
|
|
||||||
|
if (displayWidth > 0) {
|
||||||
|
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
|
||||||
|
newCursorColumn = currentOutputExternalColumn;
|
||||||
|
newCursorRow = currentOutputExternalRow;
|
||||||
|
newCursorPlaced = true;
|
||||||
|
}
|
||||||
|
currentOldCol += displayWidth;
|
||||||
|
currentOutputExternalColumn += displayWidth;
|
||||||
|
if (justToCursor && newCursorPlaced) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Old row has been copied. Check if we need to insert newline if old line was not wrapping:
|
||||||
|
if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
|
||||||
|
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||||
|
if (newCursorPlaced) newCursorRow--;
|
||||||
|
scrollDownOneLine(0, mScreenRows, currentStyle);
|
||||||
|
} else {
|
||||||
|
currentOutputExternalRow++;
|
||||||
|
}
|
||||||
|
currentOutputExternalColumn = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor[0] = newCursorColumn;
|
||||||
|
cursor[1] = newCursorRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cursor scrolling off screen:
|
||||||
|
if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
|
||||||
|
* into account.
|
||||||
|
*
|
||||||
|
* @param srcInternal The first line to be copied.
|
||||||
|
* @param len The number of lines to be copied.
|
||||||
|
*/
|
||||||
|
private void blockCopyLinesDown(int srcInternal, int len) {
|
||||||
|
if (len == 0) return;
|
||||||
|
int totalRows = mTotalRows;
|
||||||
|
|
||||||
|
int start = len - 1;
|
||||||
|
// Save away line to be overwritten:
|
||||||
|
TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows];
|
||||||
|
// Do the copy from bottom to top.
|
||||||
|
for (int i = start; i >= 0; --i)
|
||||||
|
mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows];
|
||||||
|
// Put back overwritten line, now above the block:
|
||||||
|
mLines[(srcInternal) % totalRows] = lineToBeOverWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
|
||||||
|
*
|
||||||
|
* @param topMargin First line that is scrolled.
|
||||||
|
* @param bottomMargin One line after the last line that is scrolled.
|
||||||
|
* @param style the style for the newly exposed line.
|
||||||
|
*/
|
||||||
|
public void scrollDownOneLine(int topMargin, int bottomMargin, long style) {
|
||||||
|
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
|
||||||
|
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
|
||||||
|
|
||||||
|
// Copy the fixed topMargin lines one line down so that they remain on screen in same position:
|
||||||
|
blockCopyLinesDown(mScreenFirstRow, topMargin);
|
||||||
|
// Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
|
||||||
|
// position:
|
||||||
|
blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin);
|
||||||
|
|
||||||
|
// Update the screen location in the ring buffer:
|
||||||
|
mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows;
|
||||||
|
// Note that the history has grown if not already full:
|
||||||
|
if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++;
|
||||||
|
|
||||||
|
// Blank the newly revealed line above the bottom margin:
|
||||||
|
int blankRow = externalToInternalRow(bottomMargin - 1);
|
||||||
|
if (mLines[blankRow] == null) {
|
||||||
|
mLines[blankRow] = new TerminalRow(mColumns, style);
|
||||||
|
} else {
|
||||||
|
mLines[blankRow].clear(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
|
||||||
|
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
|
||||||
|
* be thrown.
|
||||||
|
*
|
||||||
|
* @param sx source X coordinate
|
||||||
|
* @param sy source Y coordinate
|
||||||
|
* @param w width
|
||||||
|
* @param h height
|
||||||
|
* @param dx destination X coordinate
|
||||||
|
* @param dy destination Y coordinate
|
||||||
|
*/
|
||||||
|
public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
|
||||||
|
if (w == 0) return;
|
||||||
|
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
boolean copyingUp = sy > dy;
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
int y2 = copyingUp ? y : (h - (y + 1));
|
||||||
|
TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
|
||||||
|
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block set characters. All characters must be within the bounds of the screen, or else and
|
||||||
|
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
||||||
|
* of characters.
|
||||||
|
*/
|
||||||
|
public void blockSet(int sx, int sy, int w, int h, int val, long style) {
|
||||||
|
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
|
||||||
|
}
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
setChar(sx + x, sy + y, val, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TerminalRow allocateFullLineIfNecessary(int row) {
|
||||||
|
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChar(int column, int row, int codePoint, long style) {
|
||||||
|
if (row >= mScreenRows || column >= mColumns)
|
||||||
|
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||||
|
row = externalToInternalRow(row);
|
||||||
|
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStyleAt(int externalRow, int column) {
|
||||||
|
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
|
||||||
|
public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left,
|
||||||
|
int bottom, int right) {
|
||||||
|
for (int y = top; y < bottom; y++) {
|
||||||
|
TerminalRow line = mLines[externalToInternalRow(y)];
|
||||||
|
int startOfLine = (rectangular || y == top) ? left : leftMargin;
|
||||||
|
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
|
||||||
|
for (int x = startOfLine; x < endOfLine; x++) {
|
||||||
|
long currentStyle = line.getStyle(x);
|
||||||
|
int foreColor = TextStyle.decodeForeColor(currentStyle);
|
||||||
|
int backColor = TextStyle.decodeBackColor(currentStyle);
|
||||||
|
int effect = TextStyle.decodeEffect(currentStyle);
|
||||||
|
if (reverse) {
|
||||||
|
// Clear out the bits to reverse and add them back in reversed:
|
||||||
|
effect = (effect & ~bits) | (bits & ~effect);
|
||||||
|
} else if (setOrClear) {
|
||||||
|
effect |= bits;
|
||||||
|
} else {
|
||||||
|
effect &= ~bits;
|
||||||
|
}
|
||||||
|
line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
103
app/src/main/java/io/neoterm/terminal/TerminalColorScheme.java
Executable file
@ -0,0 +1,103 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
|
||||||
|
* Operating System Control (OSC) sequences.
|
||||||
|
*
|
||||||
|
* @see TerminalColors
|
||||||
|
*/
|
||||||
|
public final class TerminalColorScheme {
|
||||||
|
|
||||||
|
/** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
|
||||||
|
private static final int[] DEFAULT_COLORSCHEME = {
|
||||||
|
// 16 original colors. First 8 are dim.
|
||||||
|
0xff000000, // black
|
||||||
|
0xffcd0000, // dim red
|
||||||
|
0xff00cd00, // dim green
|
||||||
|
0xffcdcd00, // dim yellow
|
||||||
|
0xff6495ed, // dim blue
|
||||||
|
0xffcd00cd, // dim magenta
|
||||||
|
0xff00cdcd, // dim cyan
|
||||||
|
0xffe5e5e5, // dim white
|
||||||
|
// Second 8 are bright:
|
||||||
|
0xff7f7f7f, // medium grey
|
||||||
|
0xffff0000, // bright red
|
||||||
|
0xff00ff00, // bright green
|
||||||
|
0xffffff00, // bright yellow
|
||||||
|
0xff5c5cff, // light blue
|
||||||
|
0xffff00ff, // bright magenta
|
||||||
|
0xff00ffff, // bright cyan
|
||||||
|
0xffffffff, // bright white
|
||||||
|
|
||||||
|
// 216 color cube, six shades of each color:
|
||||||
|
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
|
||||||
|
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
|
||||||
|
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
|
||||||
|
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
|
||||||
|
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
|
||||||
|
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
|
||||||
|
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
|
||||||
|
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
|
||||||
|
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
|
||||||
|
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
|
||||||
|
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
|
||||||
|
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
|
||||||
|
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
|
||||||
|
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
|
||||||
|
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
|
||||||
|
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
|
||||||
|
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
|
||||||
|
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff,
|
||||||
|
|
||||||
|
// 24 grey scale ramp:
|
||||||
|
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
|
||||||
|
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
||||||
|
|
||||||
|
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||||
|
0xffffffff, 0xff000000, 0xffA9AAA9};
|
||||||
|
|
||||||
|
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||||
|
|
||||||
|
public TerminalColorScheme() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateWith(Properties props) {
|
||||||
|
reset();
|
||||||
|
for (Map.Entry<Object, Object> entries : props.entrySet()) {
|
||||||
|
String key = (String) entries.getKey();
|
||||||
|
String value = (String) entries.getValue();
|
||||||
|
int colorIndex;
|
||||||
|
|
||||||
|
if (key.equals("foreground")) {
|
||||||
|
colorIndex = TextStyle.COLOR_INDEX_FOREGROUND;
|
||||||
|
} else if (key.equals("background")) {
|
||||||
|
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
|
||||||
|
} else if (key.equals("cursor")) {
|
||||||
|
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
|
||||||
|
} else if (key.startsWith("color")) {
|
||||||
|
try {
|
||||||
|
colorIndex = Integer.parseInt(key.substring(5));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("Invalid property: '" + key + "'");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid property: '" + key + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
int colorValue = TerminalColors.parse(value);
|
||||||
|
if (colorValue == 0)
|
||||||
|
throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'");
|
||||||
|
|
||||||
|
mDefaultColors[colorIndex] = colorValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
76
app/src/main/java/io/neoterm/terminal/TerminalColors.java
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
/** Current terminal colors (if different from default). */
|
||||||
|
public final class TerminalColors {
|
||||||
|
|
||||||
|
/** Static data - a bit ugly but ok for now. */
|
||||||
|
public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC
|
||||||
|
* 4 control sequence.
|
||||||
|
*/
|
||||||
|
public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||||
|
|
||||||
|
/** Create a new instance with default colors from the theme. */
|
||||||
|
public TerminalColors() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset a particular indexed color with the default color from the color theme. */
|
||||||
|
public void reset(int index) {
|
||||||
|
mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset all indexed colors with the default color from the color theme. */
|
||||||
|
public void reset() {
|
||||||
|
System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html
|
||||||
|
* <p/>
|
||||||
|
* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed.
|
||||||
|
*/
|
||||||
|
static int parse(String c) {
|
||||||
|
try {
|
||||||
|
int skipInitial, skipBetween;
|
||||||
|
if (c.charAt(0) == '#') {
|
||||||
|
// #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits.
|
||||||
|
skipInitial = 1;
|
||||||
|
skipBetween = 0;
|
||||||
|
} else if (c.startsWith("rgb:")) {
|
||||||
|
// rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled.
|
||||||
|
skipInitial = 4;
|
||||||
|
skipBetween = 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int charsForColors = c.length() - skipInitial - 2 * skipBetween;
|
||||||
|
if (charsForColors % 3 != 0) return 0; // Unequal lengths.
|
||||||
|
int componentLength = charsForColors / 3;
|
||||||
|
double mult = 255 / (Math.pow(2, componentLength * 4) - 1);
|
||||||
|
|
||||||
|
int currentPosition = skipInitial;
|
||||||
|
String rString = c.substring(currentPosition, currentPosition + componentLength);
|
||||||
|
currentPosition += componentLength + skipBetween;
|
||||||
|
String gString = c.substring(currentPosition, currentPosition + componentLength);
|
||||||
|
currentPosition += componentLength + skipBetween;
|
||||||
|
String bString = c.substring(currentPosition, currentPosition + componentLength);
|
||||||
|
|
||||||
|
int r = (int) (Integer.parseInt(rString, 16) * mult);
|
||||||
|
int g = (int) (Integer.parseInt(gString, 16) * mult);
|
||||||
|
int b = (int) (Integer.parseInt(bString, 16) * mult);
|
||||||
|
return 0xFF << 24 | r << 16 | g << 8 | b;
|
||||||
|
} catch (NumberFormatException | IndexOutOfBoundsException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try parse a color from a text parameter and into a specified index. */
|
||||||
|
public void tryParseColor(int intoIndex, String textParameter) {
|
||||||
|
int c = parse(textParameter);
|
||||||
|
if (c != 0) mCurrentColors[intoIndex] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
2341
app/src/main/java/io/neoterm/terminal/TerminalEmulator.java
Executable file
28
app/src/main/java/io/neoterm/terminal/TerminalOutput.java
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */
|
||||||
|
public abstract class TerminalOutput {
|
||||||
|
|
||||||
|
/** Write a string using the UTF-8 encoding to the terminal client. */
|
||||||
|
public final void write(String data) {
|
||||||
|
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||||
|
write(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write bytes to the terminal client. */
|
||||||
|
public abstract void write(byte[] data, int offset, int count);
|
||||||
|
|
||||||
|
/** Notify the terminal client that the terminal title has changed. */
|
||||||
|
public abstract void titleChanged(String oldTitle, String newTitle);
|
||||||
|
|
||||||
|
/** Notify the terminal client that the terminal title has changed. */
|
||||||
|
public abstract void clipboardText(String text);
|
||||||
|
|
||||||
|
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
|
||||||
|
public abstract void onBell();
|
||||||
|
|
||||||
|
public abstract void onColorsChanged();
|
||||||
|
|
||||||
|
}
|
232
app/src/main/java/io/neoterm/terminal/TerminalRow.java
Executable file
@ -0,0 +1,232 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row in a terminal, composed of a fixed number of cells.
|
||||||
|
* <p>
|
||||||
|
* The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering.
|
||||||
|
*/
|
||||||
|
public final class TerminalRow {
|
||||||
|
|
||||||
|
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
|
||||||
|
|
||||||
|
/** The number of columns in this terminal row. */
|
||||||
|
private final int mColumns;
|
||||||
|
/** The text filling this terminal row. */
|
||||||
|
public char[] mText;
|
||||||
|
/** The number of java char:s used in {@link #mText}. */
|
||||||
|
private short mSpaceUsed;
|
||||||
|
/** If this row has been line wrapped due to text output at the end of line. */
|
||||||
|
boolean mLineWrap;
|
||||||
|
/** The style bits of each cell in the row. See {@link TextStyle}. */
|
||||||
|
final long[] mStyle;
|
||||||
|
|
||||||
|
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */
|
||||||
|
public TerminalRow(int columns, long style) {
|
||||||
|
mColumns = columns;
|
||||||
|
mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)];
|
||||||
|
mStyle = new long[columns];
|
||||||
|
clear(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** NOTE: The sourceX2 is exclusive. */
|
||||||
|
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
|
||||||
|
final int x1 = line.findStartOfColumn(sourceX1);
|
||||||
|
final int x2 = line.findStartOfColumn(sourceX2);
|
||||||
|
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
|
||||||
|
final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText;
|
||||||
|
int latestNonCombiningWidth = 0;
|
||||||
|
for (int i = x1; i < x2; i++) {
|
||||||
|
char sourceChar = sourceChars[i];
|
||||||
|
int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar;
|
||||||
|
if (startingFromSecondHalfOfWideChar) {
|
||||||
|
// Just treat copying second half of wide char as copying whitespace.
|
||||||
|
codePoint = ' ';
|
||||||
|
startingFromSecondHalfOfWideChar = false;
|
||||||
|
}
|
||||||
|
int w = WcWidth.width(codePoint);
|
||||||
|
if (w > 0) {
|
||||||
|
destinationX += latestNonCombiningWidth;
|
||||||
|
sourceX1 += latestNonCombiningWidth;
|
||||||
|
latestNonCombiningWidth = w;
|
||||||
|
}
|
||||||
|
setChar(destinationX, codePoint, line.getStyle(sourceX1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSpaceUsed() {
|
||||||
|
return mSpaceUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Note that the column may end of second half of wide character. */
|
||||||
|
public int findStartOfColumn(int column) {
|
||||||
|
if (column == mColumns) return getSpaceUsed();
|
||||||
|
|
||||||
|
int currentColumn = 0;
|
||||||
|
int currentCharIndex = 0;
|
||||||
|
while (true) { // 0<2 1 < 2
|
||||||
|
int newCharIndex = currentCharIndex;
|
||||||
|
char c = mText[newCharIndex++]; // cci=1, cci=2
|
||||||
|
boolean isHigh = Character.isHighSurrogate(c);
|
||||||
|
int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c;
|
||||||
|
int wcwidth = WcWidth.width(codePoint); // 1, 2
|
||||||
|
if (wcwidth > 0) {
|
||||||
|
currentColumn += wcwidth;
|
||||||
|
if (currentColumn == column) {
|
||||||
|
while (newCharIndex < mSpaceUsed) {
|
||||||
|
// Skip combining chars.
|
||||||
|
if (Character.isHighSurrogate(mText[newCharIndex])) {
|
||||||
|
if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) {
|
||||||
|
newCharIndex += 2;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (WcWidth.width(mText[newCharIndex]) <= 0) {
|
||||||
|
newCharIndex++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newCharIndex;
|
||||||
|
} else if (currentColumn > column) {
|
||||||
|
// Wide column going past end.
|
||||||
|
return currentCharIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentCharIndex = newCharIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean wideDisplayCharacterStartingAt(int column) {
|
||||||
|
for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed; ) {
|
||||||
|
char c = mText[currentCharIndex++];
|
||||||
|
int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c;
|
||||||
|
int wcwidth = WcWidth.width(codePoint);
|
||||||
|
if (wcwidth > 0) {
|
||||||
|
if (currentColumn == column && wcwidth == 2) return true;
|
||||||
|
currentColumn += wcwidth;
|
||||||
|
if (currentColumn > column) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(long style) {
|
||||||
|
Arrays.fill(mText, ' ');
|
||||||
|
Arrays.fill(mStyle, style);
|
||||||
|
mSpaceUsed = (short) mColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||||
|
public void setChar(int columnToSet, int codePoint, long style) {
|
||||||
|
mStyle[columnToSet] = style;
|
||||||
|
|
||||||
|
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
||||||
|
final boolean newIsCombining = newCodePointDisplayWidth <= 0;
|
||||||
|
|
||||||
|
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);
|
||||||
|
|
||||||
|
if (newIsCombining) {
|
||||||
|
// When standing at second half of wide character and inserting combining:
|
||||||
|
if (wasExtraColForWideChar) columnToSet--;
|
||||||
|
} else {
|
||||||
|
// Check if we are overwriting the second half of a wide character starting at the previous column:
|
||||||
|
if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style);
|
||||||
|
// Check if we are overwriting the first half of a wide character starting at the next column:
|
||||||
|
boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1);
|
||||||
|
if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style);
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] text = mText;
|
||||||
|
final int oldStartOfColumnIndex = findStartOfColumn(columnToSet);
|
||||||
|
final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex);
|
||||||
|
|
||||||
|
// Get the number of elements in the mText array this column uses now
|
||||||
|
int oldCharactersUsedForColumn;
|
||||||
|
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
|
||||||
|
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
|
||||||
|
} else {
|
||||||
|
// Last character.
|
||||||
|
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find how many chars this column will need
|
||||||
|
int newCharactersUsedForColumn = Character.charCount(codePoint);
|
||||||
|
if (newIsCombining) {
|
||||||
|
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
||||||
|
// modify the existing contents.
|
||||||
|
// FIXME: Put a limit of combining characters.
|
||||||
|
// FIXME: Unassigned characters also get width=0.
|
||||||
|
newCharactersUsedForColumn += oldCharactersUsedForColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn;
|
||||||
|
int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn;
|
||||||
|
|
||||||
|
final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn;
|
||||||
|
if (javaCharDifference > 0) {
|
||||||
|
// Shift the rest of the line right.
|
||||||
|
int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex;
|
||||||
|
if (mSpaceUsed + javaCharDifference > text.length) {
|
||||||
|
// We need to grow the array
|
||||||
|
char[] newText = new char[text.length + mColumns];
|
||||||
|
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
|
||||||
|
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
|
||||||
|
mText = text = newText;
|
||||||
|
} else {
|
||||||
|
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn);
|
||||||
|
}
|
||||||
|
} else if (javaCharDifference < 0) {
|
||||||
|
// Shift the rest of the line left.
|
||||||
|
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex);
|
||||||
|
}
|
||||||
|
mSpaceUsed += javaCharDifference;
|
||||||
|
|
||||||
|
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
|
||||||
|
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
|
||||||
|
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
|
||||||
|
|
||||||
|
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||||
|
// Replace second half of wide char with a space. Which mean that we actually add a ' ' java character.
|
||||||
|
if (mSpaceUsed + 1 > text.length) {
|
||||||
|
char[] newText = new char[text.length + mColumns];
|
||||||
|
System.arraycopy(text, 0, newText, 0, newNextColumnIndex);
|
||||||
|
System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
|
||||||
|
mText = text = newText;
|
||||||
|
} else {
|
||||||
|
System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
|
||||||
|
}
|
||||||
|
text[newNextColumnIndex] = ' ';
|
||||||
|
|
||||||
|
++mSpaceUsed;
|
||||||
|
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
|
||||||
|
if (columnToSet == mColumns - 1) {
|
||||||
|
throw new IllegalArgumentException("Cannot put wide character in last column");
|
||||||
|
} else if (columnToSet == mColumns - 2) {
|
||||||
|
// Truncate the line to the second part of this wide char:
|
||||||
|
mSpaceUsed = (short) newNextColumnIndex;
|
||||||
|
} else {
|
||||||
|
// Overwrite the contents of the next column, which mean we actually remove java characters. Due to the
|
||||||
|
// check at the beginning of this method we know that we are not overwriting a wide char.
|
||||||
|
int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1);
|
||||||
|
int nextLen = newNextNextColumnIndex - newNextColumnIndex;
|
||||||
|
|
||||||
|
// Shift the array leftwards.
|
||||||
|
System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex);
|
||||||
|
mSpaceUsed -= nextLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBlank() {
|
||||||
|
for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++)
|
||||||
|
if (mText[charIndex] != ' ') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final long getStyle(int column) {
|
||||||
|
return mStyle[column];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
342
app/src/main/java/io/neoterm/terminal/TerminalSession.java
Executable file
@ -0,0 +1,342 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.OsConstants;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A terminal session, consisting of a process coupled to a terminal interface.
|
||||||
|
* <p>
|
||||||
|
* The subprocess will be executed by the constructor, and when the size is made known by a call to
|
||||||
|
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
|
||||||
|
* All terminal emulation and callback methods will be performed on the main thread.
|
||||||
|
* <p>
|
||||||
|
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
|
||||||
|
* <p>
|
||||||
|
* NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks!
|
||||||
|
*/
|
||||||
|
public final class TerminalSession extends TerminalOutput {
|
||||||
|
|
||||||
|
/** Callback to be invoked when a {@link TerminalSession} changes. */
|
||||||
|
public interface SessionChangedCallback {
|
||||||
|
void onTextChanged(TerminalSession changedSession);
|
||||||
|
|
||||||
|
void onTitleChanged(TerminalSession changedSession);
|
||||||
|
|
||||||
|
void onSessionFinished(TerminalSession finishedSession);
|
||||||
|
|
||||||
|
void onClipboardText(TerminalSession session, String text);
|
||||||
|
|
||||||
|
void onBell(TerminalSession session);
|
||||||
|
|
||||||
|
void onColorsChanged(TerminalSession session);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
|
||||||
|
FileDescriptor result = new FileDescriptor();
|
||||||
|
try {
|
||||||
|
Field descriptorField;
|
||||||
|
try {
|
||||||
|
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
// For desktop java:
|
||||||
|
descriptorField = FileDescriptor.class.getDeclaredField("fd");
|
||||||
|
}
|
||||||
|
descriptorField.setAccessible(true);
|
||||||
|
descriptorField.set(result, fileDescriptor);
|
||||||
|
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||||
|
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int MSG_NEW_INPUT = 1;
|
||||||
|
private static final int MSG_PROCESS_EXITED = 4;
|
||||||
|
|
||||||
|
public final String mHandle = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
TerminalEmulator mEmulator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A queue written to from a separate thread when the process outputs, and read by main thread to process by
|
||||||
|
* terminal emulator.
|
||||||
|
*/
|
||||||
|
final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
|
||||||
|
/**
|
||||||
|
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
|
||||||
|
* writing to the {@link #mTerminalFileDescriptor}.
|
||||||
|
*/
|
||||||
|
final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
|
||||||
|
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
|
||||||
|
private final byte[] mUtf8InputBuffer = new byte[5];
|
||||||
|
|
||||||
|
/** Callback which gets notified when a session finishes or changes title. */
|
||||||
|
final SessionChangedCallback mChangeCallback;
|
||||||
|
|
||||||
|
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||||
|
int mShellPid;
|
||||||
|
|
||||||
|
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
|
||||||
|
int mShellExitStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
|
||||||
|
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
|
||||||
|
*/
|
||||||
|
private int mTerminalFileDescriptor;
|
||||||
|
|
||||||
|
/** Set by the application for user identification of session, not by terminal. */
|
||||||
|
public String mSessionName;
|
||||||
|
|
||||||
|
@SuppressLint("HandlerLeak")
|
||||||
|
final Handler mMainThreadHandler = new Handler() {
|
||||||
|
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
if (msg.what == MSG_NEW_INPUT && isRunning()) {
|
||||||
|
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||||
|
notifyScreenUpdate();
|
||||||
|
}
|
||||||
|
} else if (msg.what == MSG_PROCESS_EXITED) {
|
||||||
|
int exitCode = (Integer) msg.obj;
|
||||||
|
cleanupResources(exitCode);
|
||||||
|
mChangeCallback.onSessionFinished(TerminalSession.this);
|
||||||
|
|
||||||
|
String exitDescription = "\r\n[Process completed";
|
||||||
|
if (exitCode > 0) {
|
||||||
|
// Non-zero process exit.
|
||||||
|
exitDescription += " (code " + exitCode + ")";
|
||||||
|
} else if (exitCode < 0) {
|
||||||
|
// Negated signal.
|
||||||
|
exitDescription += " (signal " + (-exitCode) + ")";
|
||||||
|
}
|
||||||
|
exitDescription += " - press Enter]";
|
||||||
|
|
||||||
|
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||||
|
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||||
|
notifyScreenUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final String mShellPath;
|
||||||
|
private final String mCwd;
|
||||||
|
private final String[] mArgs;
|
||||||
|
private final String[] mEnv;
|
||||||
|
|
||||||
|
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
|
||||||
|
mChangeCallback = changeCallback;
|
||||||
|
|
||||||
|
this.mShellPath = shellPath;
|
||||||
|
this.mCwd = cwd;
|
||||||
|
this.mArgs = args;
|
||||||
|
this.mEnv = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||||
|
public void updateSize(int columns, int rows) {
|
||||||
|
if (mEmulator == null) {
|
||||||
|
initializeEmulator(columns, rows);
|
||||||
|
} else {
|
||||||
|
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
|
||||||
|
mEmulator.resize(columns, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The terminal title as set through escape sequences or null if none set. */
|
||||||
|
public String getTitle() {
|
||||||
|
return (mEmulator == null) ? null : mEmulator.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal emulator's window size and start terminal emulation.
|
||||||
|
*
|
||||||
|
* @param columns The number of columns in the terminal window.
|
||||||
|
* @param rows The number of rows in the terminal window.
|
||||||
|
*/
|
||||||
|
public void initializeEmulator(int columns, int rows) {
|
||||||
|
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
|
||||||
|
|
||||||
|
int[] processId = new int[1];
|
||||||
|
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||||
|
mShellPid = processId[0];
|
||||||
|
|
||||||
|
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
||||||
|
|
||||||
|
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
|
||||||
|
final byte[] buffer = new byte[4096];
|
||||||
|
while (true) {
|
||||||
|
int read = termIn.read(buffer);
|
||||||
|
if (read == -1) return;
|
||||||
|
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
|
||||||
|
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore, just shutting down.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
final byte[] buffer = new byte[4096];
|
||||||
|
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
|
||||||
|
while (true) {
|
||||||
|
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
|
||||||
|
if (bytesToWrite == -1) return;
|
||||||
|
termOut.write(buffer, 0, bytesToWrite);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int processExitCode = JNI.waitFor(mShellPid);
|
||||||
|
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write data to the shell process. */
|
||||||
|
@Override
|
||||||
|
public void write(byte[] data, int offset, int count) {
|
||||||
|
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write the Unicode code point to the terminal encoded in UTF-8. */
|
||||||
|
public void writeCodePoint(boolean prependEscape, int codePoint) {
|
||||||
|
if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
||||||
|
// 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range.
|
||||||
|
throw new IllegalArgumentException("Invalid code point: " + codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
int bufferPosition = 0;
|
||||||
|
if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27;
|
||||||
|
|
||||||
|
if (codePoint <= /* 7 bits */0b1111111) {
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) codePoint;
|
||||||
|
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
||||||
|
/* 110xxxxx leading byte with leading 5 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
||||||
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
|
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
||||||
|
/* 1110xxxx leading byte with leading 4 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
|
||||||
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||||
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
|
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
||||||
|
/* 11110xxx leading byte with leading 3 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
|
||||||
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
|
||||||
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||||
|
/* 10xxxxxx continuation byte with following 6 bits */
|
||||||
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
|
}
|
||||||
|
write(mUtf8InputBuffer, 0, bufferPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TerminalEmulator getEmulator() {
|
||||||
|
return mEmulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notify the {@link #mChangeCallback} that the screen has changed. */
|
||||||
|
protected void notifyScreenUpdate() {
|
||||||
|
mChangeCallback.onTextChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset state for terminal emulator state. */
|
||||||
|
public void reset() {
|
||||||
|
mEmulator.reset();
|
||||||
|
notifyScreenUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finish this terminal session by sending SIGKILL to the shell. */
|
||||||
|
public void finishIfRunning() {
|
||||||
|
if (isRunning()) {
|
||||||
|
try {
|
||||||
|
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
Log.w("neoterm-termux", "Failed sending SIGKILL: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cleanup resources when the process exits. */
|
||||||
|
void cleanupResources(int exitStatus) {
|
||||||
|
synchronized (this) {
|
||||||
|
mShellPid = -1;
|
||||||
|
mShellExitStatus = exitStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the reader and writer threads, and close the I/O streams
|
||||||
|
mTerminalToProcessIOQueue.close();
|
||||||
|
mProcessToTerminalIOQueue.close();
|
||||||
|
JNI.close(mTerminalFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void titleChanged(String oldTitle, String newTitle) {
|
||||||
|
mChangeCallback.onTitleChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isRunning() {
|
||||||
|
return mShellPid != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only valid if not {@link #isRunning()}. */
|
||||||
|
public synchronized int getExitStatus() {
|
||||||
|
return mShellExitStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clipboardText(String text) {
|
||||||
|
mChangeCallback.onClipboardText(this, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBell() {
|
||||||
|
mChangeCallback.onBell(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onColorsChanged() {
|
||||||
|
mChangeCallback.onColorsChanged(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPid() {
|
||||||
|
return mShellPid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
app/src/main/java/io/neoterm/terminal/TextStyle.java
Executable file
@ -0,0 +1,90 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Encodes effects, foreground and background colors into a 64 bit long, which are stored for each cell in a terminal
|
||||||
|
* row in {@link TerminalRow#mStyle}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The bit layout is:
|
||||||
|
* </p>
|
||||||
|
* - 16 flags (11 currently used).
|
||||||
|
* - 24 for foreground color (only 9 first bits if a color index).
|
||||||
|
* - 24 for background color (only 9 first bits if a color index).
|
||||||
|
*/
|
||||||
|
public final class TextStyle {
|
||||||
|
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
|
||||||
|
/**
|
||||||
|
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
|
||||||
|
* <p>
|
||||||
|
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
|
||||||
|
* come after it as erasable from the screen.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
|
||||||
|
/** Dim colors. Also known as faint or half intensity. */
|
||||||
|
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
|
||||||
|
/** If true (24-bit) color is used for the cell for foreground. */
|
||||||
|
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 << 9;
|
||||||
|
/** If true (24-bit) color is used for the cell for foreground. */
|
||||||
|
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND= 1 << 10;
|
||||||
|
|
||||||
|
public final static int COLOR_INDEX_FOREGROUND = 256;
|
||||||
|
public final static int COLOR_INDEX_BACKGROUND = 257;
|
||||||
|
public final static int COLOR_INDEX_CURSOR = 258;
|
||||||
|
|
||||||
|
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
|
||||||
|
public final static int NUM_INDEXED_COLORS = 259;
|
||||||
|
|
||||||
|
/** Normal foreground and background colors and no effects. */
|
||||||
|
final static long NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
|
||||||
|
|
||||||
|
static long encode(int foreColor, int backColor, int effect) {
|
||||||
|
long result = effect & 0b111111111;
|
||||||
|
if ((0xff000000 & foreColor) == 0xff000000) {
|
||||||
|
// 24-bit color.
|
||||||
|
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND | ((foreColor & 0x00ffffffL) << 40L);
|
||||||
|
} else {
|
||||||
|
// Indexed color.
|
||||||
|
result |= (foreColor & 0b111111111L) << 40;
|
||||||
|
}
|
||||||
|
if ((0xff000000 & backColor) == 0xff000000) {
|
||||||
|
// 24-bit color.
|
||||||
|
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND | ((backColor & 0x00ffffffL) << 16L);
|
||||||
|
} else {
|
||||||
|
// Indexed color.
|
||||||
|
result |= (backColor & 0b111111111L) << 16L;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int decodeForeColor(long style) {
|
||||||
|
if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND) == 0) {
|
||||||
|
return (int) ((style >>> 40) & 0b111111111L);
|
||||||
|
} else {
|
||||||
|
return 0xff000000 | (int) ((style >>> 40) & 0x00ffffffL);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int decodeBackColor(long style) {
|
||||||
|
if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND) == 0) {
|
||||||
|
return (int) ((style >>> 16) & 0b111111111L);
|
||||||
|
} else {
|
||||||
|
return 0xff000000 | (int) ((style >>> 16) & 0x00ffffffL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int decodeEffect(long style) {
|
||||||
|
return (int) (style & 0b11111111111);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
458
app/src/main/java/io/neoterm/terminal/WcWidth.java
Executable file
@ -0,0 +1,458 @@
|
|||||||
|
package io.neoterm.terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of wcwidth(3) for Unicode 9.
|
||||||
|
*
|
||||||
|
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||||
|
*/
|
||||||
|
public final class WcWidth {
|
||||||
|
|
||||||
|
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||||
|
// t commit 0d7de112202cc8b2ebe9232ff4a5c954f19d561a (2016-07-02):
|
||||||
|
private static final int[][] ZERO_WIDTH = {
|
||||||
|
{0x0300, 0x036f}, // Combining Grave Accent ..Combining Latin Small Le
|
||||||
|
{0x0483, 0x0489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||||
|
{0x0591, 0x05bd}, // Hebrew Accent Etnahta ..Hebrew Point Meteg
|
||||||
|
{0x05bf, 0x05bf}, // Hebrew Point Rafe ..Hebrew Point Rafe
|
||||||
|
{0x05c1, 0x05c2}, // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
|
||||||
|
{0x05c4, 0x05c5}, // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
|
||||||
|
{0x05c7, 0x05c7}, // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
|
||||||
|
{0x0610, 0x061a}, // Arabic Sign Sallallahou ..Arabic Small Kasra
|
||||||
|
{0x064b, 0x065f}, // Arabic Fathatan ..Arabic Wavy Hamza Below
|
||||||
|
{0x0670, 0x0670}, // Arabic Letter Superscrip..Arabic Letter Superscrip
|
||||||
|
{0x06d6, 0x06dc}, // Arabic Small High Ligatu..Arabic Small High Seen
|
||||||
|
{0x06df, 0x06e4}, // Arabic Small High Rounde..Arabic Small High Madda
|
||||||
|
{0x06e7, 0x06e8}, // Arabic Small High Yeh ..Arabic Small High Noon
|
||||||
|
{0x06ea, 0x06ed}, // Arabic Empty Centre Low ..Arabic Small Low Meem
|
||||||
|
{0x0711, 0x0711}, // Syriac Letter Superscrip..Syriac Letter Superscrip
|
||||||
|
{0x0730, 0x074a}, // Syriac Pthaha Above ..Syriac Barrekh
|
||||||
|
{0x07a6, 0x07b0}, // Thaana Abafili ..Thaana Sukun
|
||||||
|
{0x07eb, 0x07f3}, // Nko Combining Sh||t High..Nko Combining Double Dot
|
||||||
|
{0x0816, 0x0819}, // Samaritan Mark In ..Samaritan Mark Dagesh
|
||||||
|
{0x081b, 0x0823}, // Samaritan Mark Epentheti..Samaritan Vowel Sign A
|
||||||
|
{0x0825, 0x0827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||||
|
{0x0829, 0x082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||||
|
{0x0859, 0x085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||||
|
{0x08d4, 0x08e1}, // (nil) ..
|
||||||
|
{0x08e3, 0x0902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||||
|
{0x093a, 0x093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||||
|
{0x093c, 0x093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||||
|
{0x0941, 0x0948}, // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
|
||||||
|
{0x094d, 0x094d}, // Devanagari Sign Virama ..Devanagari Sign Virama
|
||||||
|
{0x0951, 0x0957}, // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
|
||||||
|
{0x0962, 0x0963}, // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
|
||||||
|
{0x0981, 0x0981}, // Bengali Sign Candrabindu..Bengali Sign Candrabindu
|
||||||
|
{0x09bc, 0x09bc}, // Bengali Sign Nukta ..Bengali Sign Nukta
|
||||||
|
{0x09c1, 0x09c4}, // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
|
||||||
|
{0x09cd, 0x09cd}, // Bengali Sign Virama ..Bengali Sign Virama
|
||||||
|
{0x09e2, 0x09e3}, // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
|
||||||
|
{0x0a01, 0x0a02}, // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
|
||||||
|
{0x0a3c, 0x0a3c}, // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
|
||||||
|
{0x0a41, 0x0a42}, // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
|
||||||
|
{0x0a47, 0x0a48}, // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
|
||||||
|
{0x0a4b, 0x0a4d}, // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
|
||||||
|
{0x0a51, 0x0a51}, // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
|
||||||
|
{0x0a70, 0x0a71}, // Gurmukhi Tippi ..Gurmukhi Addak
|
||||||
|
{0x0a75, 0x0a75}, // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
|
||||||
|
{0x0a81, 0x0a82}, // Gujarati Sign Candrabind..Gujarati Sign Anusvara
|
||||||
|
{0x0abc, 0x0abc}, // Gujarati Sign Nukta ..Gujarati Sign Nukta
|
||||||
|
{0x0ac1, 0x0ac5}, // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
|
||||||
|
{0x0ac7, 0x0ac8}, // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
|
||||||
|
{0x0acd, 0x0acd}, // Gujarati Sign Virama ..Gujarati Sign Virama
|
||||||
|
{0x0ae2, 0x0ae3}, // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
|
||||||
|
{0x0b01, 0x0b01}, // ||iya Sign Candrabindu ..||iya Sign Candrabindu
|
||||||
|
{0x0b3c, 0x0b3c}, // ||iya Sign Nukta ..||iya Sign Nukta
|
||||||
|
{0x0b3f, 0x0b3f}, // ||iya Vowel Sign I ..||iya Vowel Sign I
|
||||||
|
{0x0b41, 0x0b44}, // ||iya Vowel Sign U ..||iya Vowel Sign Vocalic
|
||||||
|
{0x0b4d, 0x0b4d}, // ||iya Sign Virama ..||iya Sign Virama
|
||||||
|
{0x0b56, 0x0b56}, // ||iya Ai Length Mark ..||iya Ai Length Mark
|
||||||
|
{0x0b62, 0x0b63}, // ||iya Vowel Sign Vocalic..||iya Vowel Sign Vocalic
|
||||||
|
{0x0b82, 0x0b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||||
|
{0x0bc0, 0x0bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||||
|
{0x0bcd, 0x0bcd}, // Tamil Sign Virama ..Tamil Sign Virama
|
||||||
|
{0x0c00, 0x0c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||||
|
{0x0c3e, 0x0c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||||
|
{0x0c46, 0x0c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||||
|
{0x0c4a, 0x0c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||||
|
{0x0c55, 0x0c56}, // Telugu Length Mark ..Telugu Ai Length Mark
|
||||||
|
{0x0c62, 0x0c63}, // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
|
||||||
|
{0x0c81, 0x0c81}, // Kannada Sign Candrabindu..Kannada Sign Candrabindu
|
||||||
|
{0x0cbc, 0x0cbc}, // Kannada Sign Nukta ..Kannada Sign Nukta
|
||||||
|
{0x0cbf, 0x0cbf}, // Kannada Vowel Sign I ..Kannada Vowel Sign I
|
||||||
|
{0x0cc6, 0x0cc6}, // Kannada Vowel Sign E ..Kannada Vowel Sign E
|
||||||
|
{0x0ccc, 0x0ccd}, // Kannada Vowel Sign Au ..Kannada Sign Virama
|
||||||
|
{0x0ce2, 0x0ce3}, // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
|
||||||
|
{0x0d01, 0x0d01}, // Malayalam Sign Candrabin..Malayalam Sign Candrabin
|
||||||
|
{0x0d41, 0x0d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||||
|
{0x0d4d, 0x0d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||||
|
{0x0d62, 0x0d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||||
|
{0x0dca, 0x0dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||||
|
{0x0dd2, 0x0dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||||
|
{0x0dd6, 0x0dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||||
|
{0x0e31, 0x0e31}, // Thai Character Mai Han-a..Thai Character Mai Han-a
|
||||||
|
{0x0e34, 0x0e3a}, // Thai Character Sara I ..Thai Character Phinthu
|
||||||
|
{0x0e47, 0x0e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||||
|
{0x0eb1, 0x0eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||||
|
{0x0eb4, 0x0eb9}, // Lao Vowel Sign I ..Lao Vowel Sign Uu
|
||||||
|
{0x0ebb, 0x0ebc}, // Lao Vowel Sign Mai Kon ..Lao Semivowel Sign Lo
|
||||||
|
{0x0ec8, 0x0ecd}, // Lao Tone Mai Ek ..Lao Niggahita
|
||||||
|
{0x0f18, 0x0f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||||
|
{0x0f35, 0x0f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||||
|
{0x0f37, 0x0f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||||
|
{0x0f39, 0x0f39}, // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
|
||||||
|
{0x0f71, 0x0f7e}, // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
|
||||||
|
{0x0f80, 0x0f84}, // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
|
||||||
|
{0x0f86, 0x0f87}, // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
|
||||||
|
{0x0f8d, 0x0f97}, // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
|
||||||
|
{0x0f99, 0x0fbc}, // Tibetan Subjoined Letter..Tibetan Subjoined Letter
|
||||||
|
{0x0fc6, 0x0fc6}, // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
|
||||||
|
{0x102d, 0x1030}, // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
|
||||||
|
{0x1032, 0x1037}, // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
|
||||||
|
{0x1039, 0x103a}, // Myanmar Sign Virama ..Myanmar Sign Asat
|
||||||
|
{0x103d, 0x103e}, // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||||
|
{0x1058, 0x1059}, // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
|
||||||
|
{0x105e, 0x1060}, // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||||
|
{0x1071, 0x1074}, // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
|
||||||
|
{0x1082, 0x1082}, // Myanmar Consonant Sign S..Myanmar Consonant Sign S
|
||||||
|
{0x1085, 0x1086}, // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
|
||||||
|
{0x108d, 0x108d}, // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
|
||||||
|
{0x109d, 0x109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||||
|
{0x135d, 0x135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||||
|
{0x1712, 0x1714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||||
|
{0x1732, 0x1734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
|
||||||
|
{0x1752, 0x1753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||||
|
{0x1772, 0x1773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||||
|
{0x17b4, 0x17b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||||
|
{0x17b7, 0x17bd}, // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
|
||||||
|
{0x17c6, 0x17c6}, // Khmer Sign Nikahit ..Khmer Sign Nikahit
|
||||||
|
{0x17c9, 0x17d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||||
|
{0x17dd, 0x17dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||||
|
{0x180b, 0x180d}, // Mongolian Free Variation..Mongolian Free Variation
|
||||||
|
{0x1885, 0x1886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||||
|
{0x18a9, 0x18a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||||
|
{0x1920, 0x1922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||||
|
{0x1927, 0x1928}, // Limbu Vowel Sign E ..Limbu Vowel Sign O
|
||||||
|
{0x1932, 0x1932}, // Limbu Small Letter Anusv..Limbu Small Letter Anusv
|
||||||
|
{0x1939, 0x193b}, // Limbu Sign Mukphreng ..Limbu Sign Sa-i
|
||||||
|
{0x1a17, 0x1a18}, // Buginese Vowel Sign I ..Buginese Vowel Sign U
|
||||||
|
{0x1a1b, 0x1a1b}, // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
|
||||||
|
{0x1a56, 0x1a56}, // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
|
||||||
|
{0x1a58, 0x1a5e}, // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
|
||||||
|
{0x1a60, 0x1a60}, // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
|
||||||
|
{0x1a62, 0x1a62}, // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
|
||||||
|
{0x1a65, 0x1a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||||
|
{0x1a73, 0x1a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||||
|
{0x1a7f, 0x1a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||||
|
{0x1ab0, 0x1abe}, // Combining Doubled Circum..Combining Parentheses Ov
|
||||||
|
{0x1b00, 0x1b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||||
|
{0x1b34, 0x1b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||||
|
{0x1b36, 0x1b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||||
|
{0x1b3c, 0x1b3c}, // Balinese Vowel Sign La L..Balinese Vowel Sign La L
|
||||||
|
{0x1b42, 0x1b42}, // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
|
||||||
|
{0x1b6b, 0x1b73}, // Balinese Musical Symbol ..Balinese Musical Symbol
|
||||||
|
{0x1b80, 0x1b81}, // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
|
||||||
|
{0x1ba2, 0x1ba5}, // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
|
||||||
|
{0x1ba8, 0x1ba9}, // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
|
||||||
|
{0x1bab, 0x1bad}, // Sundanese Sign Virama ..Sundanese Consonant Sign
|
||||||
|
{0x1be6, 0x1be6}, // Batak Sign Tompi ..Batak Sign Tompi
|
||||||
|
{0x1be8, 0x1be9}, // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
|
||||||
|
{0x1bed, 0x1bed}, // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
|
||||||
|
{0x1bef, 0x1bf1}, // Batak Vowel Sign U F|| S..Batak Consonant Sign H
|
||||||
|
{0x1c2c, 0x1c33}, // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
|
||||||
|
{0x1c36, 0x1c37}, // Lepcha Sign Ran ..Lepcha Sign Nukta
|
||||||
|
{0x1cd0, 0x1cd2}, // Vedic Tone Karshana ..Vedic Tone Prenkha
|
||||||
|
{0x1cd4, 0x1ce0}, // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
|
||||||
|
{0x1ce2, 0x1ce8}, // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
|
||||||
|
{0x1ced, 0x1ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||||
|
{0x1cf4, 0x1cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||||
|
{0x1cf8, 0x1cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||||
|
{0x1dc0, 0x1df5}, // Combining Dotted Grave A..Combining Up Tack Above
|
||||||
|
{0x1dfb, 0x1dff}, // (nil) ..Combining Right Arrowhea
|
||||||
|
{0x20d0, 0x20f0}, // Combining Left Harpoon A..Combining Asterisk Above
|
||||||
|
{0x2cef, 0x2cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||||
|
{0x2d7f, 0x2d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||||
|
{0x2de0, 0x2dff}, // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||||
|
{0x302a, 0x302d}, // Ideographic Level Tone M..Ideographic Entering Ton
|
||||||
|
{0x3099, 0x309a}, // Combining Katakana-hirag..Combining Katakana-hirag
|
||||||
|
{0xa66f, 0xa672}, // Combining Cyrillic Vzmet..Combining Cyrillic Thous
|
||||||
|
{0xa674, 0xa67d}, // Combining Cyrillic Lette..Combining Cyrillic Payer
|
||||||
|
{0xa69e, 0xa69f}, // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||||
|
{0xa6f0, 0xa6f1}, // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
|
||||||
|
{0xa802, 0xa802}, // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
|
||||||
|
{0xa806, 0xa806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||||
|
{0xa80b, 0xa80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||||
|
{0xa825, 0xa826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||||
|
{0xa8c4, 0xa8c5}, // Saurashtra Sign Virama ..
|
||||||
|
{0xa8e0, 0xa8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
|
||||||
|
{0xa926, 0xa92d}, // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
|
||||||
|
{0xa947, 0xa951}, // Rejang Vowel Sign I ..Rejang Consonant Sign R
|
||||||
|
{0xa980, 0xa982}, // Javanese Sign Panyangga ..Javanese Sign Layar
|
||||||
|
{0xa9b3, 0xa9b3}, // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
|
||||||
|
{0xa9b6, 0xa9b9}, // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
|
||||||
|
{0xa9bc, 0xa9bc}, // Javanese Vowel Sign Pepe..Javanese Vowel Sign Pepe
|
||||||
|
{0xa9e5, 0xa9e5}, // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
|
||||||
|
{0xaa29, 0xaa2e}, // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
|
||||||
|
{0xaa31, 0xaa32}, // Cham Vowel Sign Au ..Cham Vowel Sign Ue
|
||||||
|
{0xaa35, 0xaa36}, // Cham Consonant Sign La ..Cham Consonant Sign Wa
|
||||||
|
{0xaa43, 0xaa43}, // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||||
|
{0xaa4c, 0xaa4c}, // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||||
|
{0xaa7c, 0xaa7c}, // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
|
||||||
|
{0xaab0, 0xaab0}, // Tai Viet Mai Kang ..Tai Viet Mai Kang
|
||||||
|
{0xaab2, 0xaab4}, // Tai Viet Vowel I ..Tai Viet Vowel U
|
||||||
|
{0xaab7, 0xaab8}, // Tai Viet Mai Khit ..Tai Viet Vowel Ia
|
||||||
|
{0xaabe, 0xaabf}, // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
|
||||||
|
{0xaac1, 0xaac1}, // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
|
||||||
|
{0xaaec, 0xaaed}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||||
|
{0xaaf6, 0xaaf6}, // Meetei Mayek Virama ..Meetei Mayek Virama
|
||||||
|
{0xabe5, 0xabe5}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||||
|
{0xabe8, 0xabe8}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||||
|
{0xabed, 0xabed}, // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
|
||||||
|
{0xfb1e, 0xfb1e}, // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
|
||||||
|
{0xfe00, 0xfe0f}, // Variation Select||-1 ..Variation Select||-16
|
||||||
|
{0xfe20, 0xfe2f}, // Combining Ligature Left ..Combining Cyrillic Titlo
|
||||||
|
{0x101fd, 0x101fd}, // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
|
||||||
|
{0x102e0, 0x102e0}, // Coptic Epact Thousands M..Coptic Epact Thousands M
|
||||||
|
{0x10376, 0x1037a}, // Combining Old Permic Let..Combining Old Permic Let
|
||||||
|
{0x10a01, 0x10a03}, // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
|
||||||
|
{0x10a05, 0x10a06}, // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
|
||||||
|
{0x10a0c, 0x10a0f}, // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
|
||||||
|
{0x10a38, 0x10a3a}, // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
|
||||||
|
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
|
||||||
|
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||||
|
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||||
|
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||||
|
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||||
|
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||||
|
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||||
|
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||||
|
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||||
|
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||||
|
{0x11173, 0x11173}, // Mahajani Sign Nukta ..Mahajani Sign Nukta
|
||||||
|
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||||
|
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||||
|
{0x111ca, 0x111cc}, // Sharada Sign Nukta ..Sharada Extra Sh||t Vowe
|
||||||
|
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||||
|
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||||
|
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||||
|
{0x1123e, 0x1123e}, // (nil) ..
|
||||||
|
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||||
|
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||||
|
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||||
|
{0x1133c, 0x1133c}, // Grantha Sign Nukta ..Grantha Sign Nukta
|
||||||
|
{0x11340, 0x11340}, // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
|
||||||
|
{0x11366, 0x1136c}, // Combining Grantha Digit ..Combining Grantha Digit
|
||||||
|
{0x11370, 0x11374}, // Combining Grantha Letter..Combining Grantha Letter
|
||||||
|
{0x11438, 0x1143f}, // (nil) ..
|
||||||
|
{0x11442, 0x11444}, // (nil) ..
|
||||||
|
{0x11446, 0x11446}, // (nil) ..
|
||||||
|
{0x114b3, 0x114b8}, // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
|
||||||
|
{0x114ba, 0x114ba}, // Tirhuta Vowel Sign Sh||t..Tirhuta Vowel Sign Sh||t
|
||||||
|
{0x114bf, 0x114c0}, // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
|
||||||
|
{0x114c2, 0x114c3}, // Tirhuta Sign Virama ..Tirhuta Sign Nukta
|
||||||
|
{0x115b2, 0x115b5}, // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
|
||||||
|
{0x115bc, 0x115bd}, // Siddham Sign Candrabindu..Siddham Sign Anusvara
|
||||||
|
{0x115bf, 0x115c0}, // Siddham Sign Virama ..Siddham Sign Nukta
|
||||||
|
{0x115dc, 0x115dd}, // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
|
||||||
|
{0x11633, 0x1163a}, // Modi Vowel Sign U ..Modi Vowel Sign Ai
|
||||||
|
{0x1163d, 0x1163d}, // Modi Sign Anusvara ..Modi Sign Anusvara
|
||||||
|
{0x1163f, 0x11640}, // Modi Sign Virama ..Modi Sign Ardhacandra
|
||||||
|
{0x116ab, 0x116ab}, // Takri Sign Anusvara ..Takri Sign Anusvara
|
||||||
|
{0x116ad, 0x116ad}, // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
|
||||||
|
{0x116b0, 0x116b5}, // Takri Vowel Sign U ..Takri Vowel Sign Au
|
||||||
|
{0x116b7, 0x116b7}, // Takri Sign Nukta ..Takri Sign Nukta
|
||||||
|
{0x1171d, 0x1171f}, // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
|
||||||
|
{0x11722, 0x11725}, // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
|
||||||
|
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||||
|
{0x11c30, 0x11c36}, // (nil) ..
|
||||||
|
{0x11c38, 0x11c3d}, // (nil) ..
|
||||||
|
{0x11c3f, 0x11c3f}, // (nil) ..
|
||||||
|
{0x11c92, 0x11ca7}, // (nil) ..
|
||||||
|
{0x11caa, 0x11cb0}, // (nil) ..
|
||||||
|
{0x11cb2, 0x11cb3}, // (nil) ..
|
||||||
|
{0x11cb5, 0x11cb6}, // (nil) ..
|
||||||
|
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
|
||||||
|
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||||
|
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
|
||||||
|
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
|
||||||
|
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
|
||||||
|
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
|
||||||
|
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
|
||||||
|
{0x1d1aa, 0x1d1ad}, // Musical Symbol Combining..Musical Symbol Combining
|
||||||
|
{0x1d242, 0x1d244}, // Combining Greek Musical ..Combining Greek Musical
|
||||||
|
{0x1da00, 0x1da36}, // Signwriting Head Rim ..Signwriting Air Sucking
|
||||||
|
{0x1da3b, 0x1da6c}, // Signwriting Mouth Closed..Signwriting Excitement
|
||||||
|
{0x1da75, 0x1da75}, // Signwriting Upper Body T..Signwriting Upper Body T
|
||||||
|
{0x1da84, 0x1da84}, // Signwriting Location Hea..Signwriting Location Hea
|
||||||
|
{0x1da9b, 0x1da9f}, // Signwriting Fill Modifie..Signwriting Fill Modifie
|
||||||
|
{0x1daa1, 0x1daaf}, // Signwriting Rotation Mod..Signwriting Rotation Mod
|
||||||
|
{0x1e000, 0x1e006}, // (nil) ..
|
||||||
|
{0x1e008, 0x1e018}, // (nil) ..
|
||||||
|
{0x1e01b, 0x1e021}, // (nil) ..
|
||||||
|
{0x1e023, 0x1e024}, // (nil) ..
|
||||||
|
{0x1e026, 0x1e02a}, // (nil) ..
|
||||||
|
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||||
|
{0x1e944, 0x1e94a}, // (nil) ..
|
||||||
|
{0xe0100, 0xe01ef}, // Variation Select||-17 ..Variation Select||-256
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||||
|
// at commit 0d7de112202cc8b2ebe9232ff4a5c954f19d561a (2016-07-02):
|
||||||
|
private static final int[][] WIDE_EASTASIAN = {
|
||||||
|
{0x1100, 0x115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||||
|
{0x231a, 0x231b}, // Watch ..Hourglass
|
||||||
|
{0x2329, 0x232a}, // Left-pointing Angle Brac..Right-pointing Angle Bra
|
||||||
|
{0x23e9, 0x23ec}, // Black Right-pointing Dou..Black Down-pointing Doub
|
||||||
|
{0x23f0, 0x23f0}, // Alarm Clock ..Alarm Clock
|
||||||
|
{0x23f3, 0x23f3}, // Hourglass With Flowing S..Hourglass With Flowing S
|
||||||
|
{0x25fd, 0x25fe}, // White Medium Small Squar..Black Medium Small Squar
|
||||||
|
{0x2614, 0x2615}, // Umbrella With Rain Drops..Hot Beverage
|
||||||
|
{0x2648, 0x2653}, // Aries ..Pisces
|
||||||
|
{0x267f, 0x267f}, // Wheelchair Symbol ..Wheelchair Symbol
|
||||||
|
{0x2693, 0x2693}, // Anch|| ..Anch||
|
||||||
|
{0x26a1, 0x26a1}, // High Voltage Sign ..High Voltage Sign
|
||||||
|
{0x26aa, 0x26ab}, // Medium White Circle ..Medium Black Circle
|
||||||
|
{0x26bd, 0x26be}, // Soccer Ball ..Baseball
|
||||||
|
{0x26c4, 0x26c5}, // Snowman Without Snow ..Sun Behind Cloud
|
||||||
|
{0x26ce, 0x26ce}, // Ophiuchus ..Ophiuchus
|
||||||
|
{0x26d4, 0x26d4}, // No Entry ..No Entry
|
||||||
|
{0x26ea, 0x26ea}, // Church ..Church
|
||||||
|
{0x26f2, 0x26f3}, // Fountain ..Flag In Hole
|
||||||
|
{0x26f5, 0x26f5}, // Sailboat ..Sailboat
|
||||||
|
{0x26fa, 0x26fa}, // Tent ..Tent
|
||||||
|
{0x26fd, 0x26fd}, // Fuel Pump ..Fuel Pump
|
||||||
|
{0x2705, 0x2705}, // White Heavy Check Mark ..White Heavy Check Mark
|
||||||
|
{0x270a, 0x270b}, // Raised Fist ..Raised Hand
|
||||||
|
{0x2728, 0x2728}, // Sparkles ..Sparkles
|
||||||
|
{0x274c, 0x274c}, // Cross Mark ..Cross Mark
|
||||||
|
{0x274e, 0x274e}, // Negative Squared Cross M..Negative Squared Cross M
|
||||||
|
{0x2753, 0x2755}, // Black Question Mark ||na..White Exclamation Mark O
|
||||||
|
{0x2757, 0x2757}, // Heavy Exclamation Mark S..Heavy Exclamation Mark S
|
||||||
|
{0x2795, 0x2797}, // Heavy Plus Sign ..Heavy Division Sign
|
||||||
|
{0x27b0, 0x27b0}, // Curly Loop ..Curly Loop
|
||||||
|
{0x27bf, 0x27bf}, // Double Curly Loop ..Double Curly Loop
|
||||||
|
{0x2b1b, 0x2b1c}, // Black Large Square ..White Large Square
|
||||||
|
{0x2b50, 0x2b50}, // White Medium Star ..White Medium Star
|
||||||
|
{0x2b55, 0x2b55}, // Heavy Large Circle ..Heavy Large Circle
|
||||||
|
{0x2e80, 0x2e99}, // Cjk Radical Repeat ..Cjk Radical Rap
|
||||||
|
{0x2e9b, 0x2ef3}, // Cjk Radical Choke ..Cjk Radical C-simplified
|
||||||
|
{0x2f00, 0x2fd5}, // Kangxi Radical One ..Kangxi Radical Flute
|
||||||
|
{0x2ff0, 0x2ffb}, // Ideographic Description ..Ideographic Description
|
||||||
|
{0x3000, 0x303e}, // Ideographic Space ..Ideographic Variation In
|
||||||
|
{0x3041, 0x3096}, // Hiragana Letter Small A ..Hiragana Letter Small Ke
|
||||||
|
{0x3099, 0x30ff}, // Combining Katakana-hirag..Katakana Digraph Koto
|
||||||
|
{0x3105, 0x312d}, // Bopomofo Letter B ..Bopomofo Letter Ih
|
||||||
|
{0x3131, 0x318e}, // Hangul Letter Kiyeok ..Hangul Letter Araeae
|
||||||
|
{0x3190, 0x31ba}, // Ideographic Annotation L..Bopomofo Letter Zy
|
||||||
|
{0x31c0, 0x31e3}, // Cjk Stroke T ..Cjk Stroke Q
|
||||||
|
{0x31f0, 0x321e}, // Katakana Letter Small Ku..Parenthesized K||ean Cha
|
||||||
|
{0x3220, 0x3247}, // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||||
|
{0x3250, 0x32fe}, // Partnership Sign ..Circled Katakana Wo
|
||||||
|
{0x3300, 0x4dbf}, // Square Apaato ..
|
||||||
|
{0x4e00, 0xa48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||||
|
{0xa490, 0xa4c6}, // Yi Radical Qot ..Yi Radical Ke
|
||||||
|
{0xa960, 0xa97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||||
|
{0xac00, 0xd7a3}, // Hangul Syllable Ga ..Hangul Syllable Hih
|
||||||
|
{0xf900, 0xfaff}, // Cjk Compatibility Ideogr..
|
||||||
|
{0xfe10, 0xfe19}, // Presentation F||m F|| Ve..Presentation F||m F|| Ve
|
||||||
|
{0xfe30, 0xfe52}, // Presentation F||m F|| Ve..Small Full Stop
|
||||||
|
{0xfe54, 0xfe66}, // Small Semicolon ..Small Equals Sign
|
||||||
|
{0xfe68, 0xfe6b}, // Small Reverse Solidus ..Small Commercial At
|
||||||
|
{0xff01, 0xff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||||
|
{0xffe0, 0xffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||||
|
{0x16fe0, 0x16fe0}, // (nil) ..
|
||||||
|
{0x17000, 0x187ec}, // (nil) ..
|
||||||
|
{0x18800, 0x18af2}, // (nil) ..
|
||||||
|
{0x1b000, 0x1b001}, // Katakana Letter Archaic ..Hiragana Letter Archaic
|
||||||
|
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||||
|
{0x1f0cf, 0x1f0cf}, // Playing Card Black Joker..Playing Card Black Joker
|
||||||
|
{0x1f18e, 0x1f18e}, // Negative Squared Ab ..Negative Squared Ab
|
||||||
|
{0x1f191, 0x1f19a}, // Squared Cl ..Squared Vs
|
||||||
|
{0x1f200, 0x1f202}, // Square Hiragana Hoka ..Squared Katakana Sa
|
||||||
|
{0x1f210, 0x1f23b}, // Squared Cjk Unified Ideo..
|
||||||
|
{0x1f240, 0x1f248}, // T||toise Shell Bracketed..T||toise Shell Bracketed
|
||||||
|
{0x1f250, 0x1f251}, // Circled Ideograph Advant..Circled Ideograph Accept
|
||||||
|
{0x1f300, 0x1f320}, // Cyclone ..Shooting Star
|
||||||
|
{0x1f32d, 0x1f335}, // Hot Dog ..Cactus
|
||||||
|
{0x1f337, 0x1f37c}, // Tulip ..Baby Bottle
|
||||||
|
{0x1f37e, 0x1f393}, // Bottle With Popping C||k..Graduation Cap
|
||||||
|
{0x1f3a0, 0x1f3ca}, // Carousel H||se ..Swimmer
|
||||||
|
{0x1f3cf, 0x1f3d3}, // Cricket Bat And Ball ..Table Tennis Paddle And
|
||||||
|
{0x1f3e0, 0x1f3f0}, // House Building ..European Castle
|
||||||
|
{0x1f3f4, 0x1f3f4}, // Waving Black Flag ..Waving Black Flag
|
||||||
|
{0x1f3f8, 0x1f43e}, // Badminton Racquet And Sh..Paw Prints
|
||||||
|
{0x1f440, 0x1f440}, // Eyes ..Eyes
|
||||||
|
{0x1f442, 0x1f4fc}, // Ear ..Videocassette
|
||||||
|
{0x1f4ff, 0x1f53d}, // Prayer Beads ..Down-pointing Small Red
|
||||||
|
{0x1f54b, 0x1f54e}, // Kaaba ..Men||ah With Nine Branch
|
||||||
|
{0x1f550, 0x1f567}, // Clock Face One Oclock ..Clock Face Twelve-thirty
|
||||||
|
{0x1f57a, 0x1f57a}, // (nil) ..
|
||||||
|
{0x1f595, 0x1f596}, // Reversed Hand With Middl..Raised Hand With Part Be
|
||||||
|
{0x1f5a4, 0x1f5a4}, // (nil) ..
|
||||||
|
{0x1f5fb, 0x1f64f}, // Mount Fuji ..Person With Folded Hands
|
||||||
|
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
|
||||||
|
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
|
||||||
|
{0x1f6d0, 0x1f6d2}, // Place Of W||ship ..
|
||||||
|
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
|
||||||
|
{0x1f6f4, 0x1f6f6}, // (nil) ..
|
||||||
|
{0x1f910, 0x1f91e}, // Zipper-mouth Face ..
|
||||||
|
{0x1f920, 0x1f927}, // (nil) ..
|
||||||
|
{0x1f930, 0x1f930}, // (nil) ..
|
||||||
|
{0x1f933, 0x1f93e}, // (nil) ..
|
||||||
|
{0x1f940, 0x1f94b}, // (nil) ..
|
||||||
|
{0x1f950, 0x1f95e}, // (nil) ..
|
||||||
|
{0x1f980, 0x1f991}, // Crab ..
|
||||||
|
{0x1f9c0, 0x1f9c0}, // Cheese Wedge ..Cheese Wedge
|
||||||
|
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..
|
||||||
|
{0x30000, 0x3fffd}, // (nil) ..
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean intable(int[][] table, int c) {
|
||||||
|
// First quick check f|| Latin1 etc. characters.
|
||||||
|
if (c < table[0][0]) return false;
|
||||||
|
|
||||||
|
// Binary search in table.
|
||||||
|
int bot = 0;
|
||||||
|
int top = table.length - 1; // (int)(size / sizeof(struct interval) - 1);
|
||||||
|
while (top >= bot) {
|
||||||
|
int mid = (bot + top) / 2;
|
||||||
|
if (table[mid][1] < c) {
|
||||||
|
bot = mid + 1;
|
||||||
|
} else if (table[mid][0] > c) {
|
||||||
|
top = mid - 1;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return the terminal display width of a code point: 0, 1 || 2. */
|
||||||
|
public static int width(int ucs) {
|
||||||
|
if (ucs == 0 ||
|
||||||
|
ucs == 0x034F ||
|
||||||
|
(0x200B <= ucs && ucs <= 0x200F) ||
|
||||||
|
ucs == 0x2028 ||
|
||||||
|
ucs == 0x2029 ||
|
||||||
|
(0x202A <= ucs && ucs <= 0x202E) ||
|
||||||
|
(0x2060 <= ucs && ucs <= 0x2063)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// C0/C1 control characters
|
||||||
|
// Termux change: Return 0 instead of -1.
|
||||||
|
if (ucs < 32 || (0x07F <= ucs && ucs < 0x0A0)) return 0;
|
||||||
|
|
||||||
|
// combining characters with zero width
|
||||||
|
if (intable(ZERO_WIDTH, ucs)) return 0;
|
||||||
|
|
||||||
|
return intable(WIDE_EASTASIAN, ucs) ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The width at an index position in a java char array. */
|
||||||
|
public static int width(char[] chars, int index) {
|
||||||
|
char c = chars[index];
|
||||||
|
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
178
app/src/main/java/io/neoterm/view/ExtraKeysView.java
Executable file
@ -0,0 +1,178 @@
|
|||||||
|
package io.neoterm.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
import io.neoterm.R;
|
||||||
|
import io.neoterm.terminal.TerminalSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||||
|
* keyboard.
|
||||||
|
*/
|
||||||
|
public final class ExtraKeysView extends GridLayout {
|
||||||
|
|
||||||
|
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendKey(View view, String keyName) {
|
||||||
|
int keyCode = 0;
|
||||||
|
String chars = null;
|
||||||
|
switch (keyName) {
|
||||||
|
case "ESC":
|
||||||
|
keyCode = KeyEvent.KEYCODE_ESCAPE;
|
||||||
|
break;
|
||||||
|
case "TAB":
|
||||||
|
keyCode = KeyEvent.KEYCODE_TAB;
|
||||||
|
break;
|
||||||
|
case "▲":
|
||||||
|
keyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||||
|
break;
|
||||||
|
case "◀":
|
||||||
|
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||||
|
break;
|
||||||
|
case "▶":
|
||||||
|
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||||
|
break;
|
||||||
|
case "▼":
|
||||||
|
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||||
|
break;
|
||||||
|
case "―":
|
||||||
|
chars = "-";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
chars = keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode > 0) {
|
||||||
|
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||||
|
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||||
|
} else {
|
||||||
|
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
|
||||||
|
TerminalSession session = terminalView.getCurrentSession();
|
||||||
|
if (session != null) session.write(chars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToggleButton controlButton;
|
||||||
|
private ToggleButton altButton;
|
||||||
|
private ToggleButton fnButton;
|
||||||
|
|
||||||
|
public boolean readControlButton() {
|
||||||
|
if (controlButton.isPressed()) return true;
|
||||||
|
boolean result = controlButton.isChecked();
|
||||||
|
if (result) {
|
||||||
|
controlButton.setChecked(false);
|
||||||
|
controlButton.setTextColor(TEXT_COLOR);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean readAltButton() {
|
||||||
|
if (altButton.isPressed()) return true;
|
||||||
|
boolean result = altButton.isChecked();
|
||||||
|
if (result) {
|
||||||
|
altButton.setChecked(false);
|
||||||
|
altButton.setTextColor(TEXT_COLOR);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean readFnButton() {
|
||||||
|
if (fnButton.isPressed()) return true;
|
||||||
|
boolean result = fnButton.isChecked();
|
||||||
|
if (result) {
|
||||||
|
fnButton.setChecked(false);
|
||||||
|
fnButton.setTextColor(TEXT_COLOR);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reload() {
|
||||||
|
altButton = controlButton = null;
|
||||||
|
removeAllViews();
|
||||||
|
|
||||||
|
String[][] buttons = {
|
||||||
|
{"ESC", "CTRL", "ALT", "TAB", "―", "/", "|"}
|
||||||
|
};
|
||||||
|
|
||||||
|
final int rows = buttons.length;
|
||||||
|
final int cols = buttons[0].length;
|
||||||
|
|
||||||
|
setRowCount(rows);
|
||||||
|
setColumnCount(cols);
|
||||||
|
|
||||||
|
for (int row = 0; row < rows; row++) {
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
final String buttonText = buttons[row][col];
|
||||||
|
|
||||||
|
Button button;
|
||||||
|
switch (buttonText) {
|
||||||
|
case "CTRL":
|
||||||
|
button = controlButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setClickable(true);
|
||||||
|
break;
|
||||||
|
case "ALT":
|
||||||
|
button = altButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setClickable(true);
|
||||||
|
break;
|
||||||
|
case "FN":
|
||||||
|
button = fnButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setClickable(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.setText(buttonText);
|
||||||
|
button.setTextColor(TEXT_COLOR);
|
||||||
|
|
||||||
|
final Button finalButton = button;
|
||||||
|
button.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
|
View root = getRootView();
|
||||||
|
switch (buttonText) {
|
||||||
|
case "CTRL":
|
||||||
|
case "ALT":
|
||||||
|
case "FN":
|
||||||
|
ToggleButton self = (ToggleButton) finalButton;
|
||||||
|
self.setChecked(self.isChecked());
|
||||||
|
self.setTextColor(self.isChecked() ? 0xFF80DEEA : TEXT_COLOR);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendKey(root, buttonText);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LayoutParams param = new LayoutParams();
|
||||||
|
param.height = param.width = 0;
|
||||||
|
param.rightMargin = param.topMargin = 0;
|
||||||
|
param.setGravity(Gravity.LEFT);
|
||||||
|
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f;
|
||||||
|
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, weight);
|
||||||
|
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||||
|
button.setLayoutParams(param);
|
||||||
|
|
||||||
|
addView(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
111
app/src/main/java/io/neoterm/view/GestureAndScaleRecognizer.java
Executable file
@ -0,0 +1,111 @@
|
|||||||
|
package io.neoterm.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.ScaleGestureDetector;
|
||||||
|
|
||||||
|
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
|
||||||
|
final class GestureAndScaleRecognizer {
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
boolean onSingleTapUp(MotionEvent e);
|
||||||
|
|
||||||
|
boolean onDoubleTap(MotionEvent e);
|
||||||
|
|
||||||
|
boolean onScroll(MotionEvent e2, float dx, float dy);
|
||||||
|
|
||||||
|
boolean onFling(MotionEvent e, float velocityX, float velocityY);
|
||||||
|
|
||||||
|
boolean onScale(float focusX, float focusY, float scale);
|
||||||
|
|
||||||
|
boolean onDown(float x, float y);
|
||||||
|
|
||||||
|
boolean onUp(MotionEvent e);
|
||||||
|
|
||||||
|
void onLongPress(MotionEvent e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final GestureDetector mGestureDetector;
|
||||||
|
private final ScaleGestureDetector mScaleDetector;
|
||||||
|
final Listener mListener;
|
||||||
|
boolean isAfterLongPress;
|
||||||
|
|
||||||
|
public GestureAndScaleRecognizer(Context context, Listener listener) {
|
||||||
|
mListener = listener;
|
||||||
|
|
||||||
|
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
|
||||||
|
return mListener.onScroll(e2, dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||||
|
return mListener.onFling(e2, velocityX, velocityY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(MotionEvent e) {
|
||||||
|
return mListener.onDown(e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongPress(MotionEvent e) {
|
||||||
|
mListener.onLongPress(e);
|
||||||
|
isAfterLongPress = true;
|
||||||
|
}
|
||||||
|
}, null, true /* ignoreMultitouch */);
|
||||||
|
|
||||||
|
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||||
|
return mListener.onSingleTapUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDoubleTap(MotionEvent e) {
|
||||||
|
return mListener.onDoubleTap(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onScale(ScaleGestureDetector detector) {
|
||||||
|
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTouchEvent(MotionEvent event) {
|
||||||
|
mGestureDetector.onTouchEvent(event);
|
||||||
|
mScaleDetector.onTouchEvent(event);
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
isAfterLongPress = false;
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
if (!isAfterLongPress) {
|
||||||
|
// This behaviour is desired when in e.g. vim with mouse events, where we do not
|
||||||
|
// want to move the cursor when lifting finger after a long press.
|
||||||
|
mListener.onUp(event);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInProgress() {
|
||||||
|
return mScaleDetector.isInProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
230
app/src/main/java/io/neoterm/view/TerminalRenderer.java
Executable file
@ -0,0 +1,230 @@
|
|||||||
|
package io.neoterm.view;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
|
||||||
|
import io.neoterm.terminal.TerminalBuffer;
|
||||||
|
import io.neoterm.terminal.TerminalEmulator;
|
||||||
|
import io.neoterm.terminal.TerminalRow;
|
||||||
|
import io.neoterm.terminal.TextStyle;
|
||||||
|
import io.neoterm.terminal.WcWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderer of a {@link TerminalEmulator} into a {@link Canvas}.
|
||||||
|
* <p/>
|
||||||
|
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||||
|
*/
|
||||||
|
final class TerminalRenderer {
|
||||||
|
|
||||||
|
final int mTextSize;
|
||||||
|
final Typeface mTypeface;
|
||||||
|
private final Paint mTextPaint = new Paint();
|
||||||
|
|
||||||
|
/** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */
|
||||||
|
final float mFontWidth;
|
||||||
|
/** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||||
|
final int mFontLineSpacing;
|
||||||
|
/** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||||
|
private final int mFontAscent;
|
||||||
|
/** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */
|
||||||
|
final int mFontLineSpacingAndAscent;
|
||||||
|
|
||||||
|
private final float[] asciiMeasures = new float[127];
|
||||||
|
|
||||||
|
public TerminalRenderer(int textSize, Typeface typeface) {
|
||||||
|
mTextSize = textSize;
|
||||||
|
mTypeface = typeface;
|
||||||
|
|
||||||
|
mTextPaint.setTypeface(typeface);
|
||||||
|
mTextPaint.setAntiAlias(true);
|
||||||
|
mTextPaint.setTextSize(textSize);
|
||||||
|
|
||||||
|
mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing());
|
||||||
|
mFontAscent = (int) Math.ceil(mTextPaint.ascent());
|
||||||
|
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent;
|
||||||
|
mFontWidth = mTextPaint.measureText("X");
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(" ");
|
||||||
|
for (int i = 0; i < asciiMeasures.length; i++) {
|
||||||
|
sb.setCharAt(0, (char) i);
|
||||||
|
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
|
||||||
|
public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow,
|
||||||
|
int selectionY1, int selectionY2, int selectionX1, int selectionX2) {
|
||||||
|
final boolean reverseVideo = mEmulator.isReverseVideo();
|
||||||
|
final int endRow = topRow + mEmulator.mRows;
|
||||||
|
final int columns = mEmulator.mColumns;
|
||||||
|
final int cursorCol = mEmulator.getCursorCol();
|
||||||
|
final int cursorRow = mEmulator.getCursorRow();
|
||||||
|
final boolean cursorVisible = mEmulator.isShowingCursor();
|
||||||
|
final TerminalBuffer screen = mEmulator.getScreen();
|
||||||
|
final int[] palette = mEmulator.mColors.mCurrentColors;
|
||||||
|
final int cursorShape = mEmulator.getCursorStyle();
|
||||||
|
|
||||||
|
if (reverseVideo)
|
||||||
|
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC);
|
||||||
|
|
||||||
|
float heightOffset = mFontLineSpacingAndAscent;
|
||||||
|
for (int row = topRow; row < endRow; row++) {
|
||||||
|
heightOffset += mFontLineSpacing;
|
||||||
|
|
||||||
|
final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1;
|
||||||
|
int selx1 = -1, selx2 = -1;
|
||||||
|
if (row >= selectionY1 && row <= selectionY2) {
|
||||||
|
if (row == selectionY1) selx1 = selectionX1;
|
||||||
|
selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
|
||||||
|
final char[] line = lineObject.mText;
|
||||||
|
final int charsUsedInLine = lineObject.getSpaceUsed();
|
||||||
|
|
||||||
|
long lastRunStyle = 0;
|
||||||
|
boolean lastRunInsideCursor = false;
|
||||||
|
int lastRunStartColumn = -1;
|
||||||
|
int lastRunStartIndex = 0;
|
||||||
|
boolean lastRunFontWidthMismatch = false;
|
||||||
|
int currentCharIndex = 0;
|
||||||
|
float measuredWidthForRun = 0.f;
|
||||||
|
|
||||||
|
for (int column = 0; column < columns; ) {
|
||||||
|
final char charAtIndex = line[currentCharIndex];
|
||||||
|
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
|
||||||
|
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
|
||||||
|
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
|
||||||
|
final int codePointWcWidth = WcWidth.width(codePoint);
|
||||||
|
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
|
||||||
|
final long style = lineObject.getStyle(column);
|
||||||
|
|
||||||
|
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
|
||||||
|
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
|
||||||
|
// smileys which android font renders as wide.
|
||||||
|
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
|
||||||
|
final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line,
|
||||||
|
currentCharIndex, charsForCodePoint);
|
||||||
|
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
|
||||||
|
|
||||||
|
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) {
|
||||||
|
if (column == 0) {
|
||||||
|
// Skip first column as there is nothing to draw, just record the current style.
|
||||||
|
} else {
|
||||||
|
final int columnWidthSinceLastRun = column - lastRunStartColumn;
|
||||||
|
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||||
|
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||||
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||||
|
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||||
|
cursorColor, cursorShape, lastRunStyle, reverseVideo);
|
||||||
|
}
|
||||||
|
measuredWidthForRun = 0.f;
|
||||||
|
lastRunStyle = style;
|
||||||
|
lastRunInsideCursor = insideCursor;
|
||||||
|
lastRunStartColumn = column;
|
||||||
|
lastRunStartIndex = currentCharIndex;
|
||||||
|
lastRunFontWidthMismatch = fontWidthMismatch;
|
||||||
|
}
|
||||||
|
measuredWidthForRun += measuredCodePointWidth;
|
||||||
|
column += codePointWcWidth;
|
||||||
|
currentCharIndex += charsForCodePoint;
|
||||||
|
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
|
||||||
|
// Eat combining chars so that they are treated as part of the last non-combining code point,
|
||||||
|
// instead of e.g. being considered inside the cursor in the next run.
|
||||||
|
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
|
||||||
|
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||||
|
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||||
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
||||||
|
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns,
|
||||||
|
int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle,
|
||||||
|
long textStyle, boolean reverseVideo) {
|
||||||
|
int foreColor = TextStyle.decodeForeColor(textStyle);
|
||||||
|
final int effect = TextStyle.decodeEffect(textStyle);
|
||||||
|
int backColor = TextStyle.decodeBackColor(textStyle);
|
||||||
|
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
|
||||||
|
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
|
||||||
|
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
|
||||||
|
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
|
||||||
|
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
|
||||||
|
|
||||||
|
if ((foreColor & 0xff000000) != 0xff000000) {
|
||||||
|
// Let bold have bright colors if applicable (one of the first 8):
|
||||||
|
if (bold && foreColor >= 0 && foreColor < 8) foreColor += 8;
|
||||||
|
foreColor = palette[foreColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((backColor & 0xff000000) != 0xff000000) {
|
||||||
|
backColor = palette[backColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse video here if _one and only one_ of the reverse flags are set:
|
||||||
|
final boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
|
||||||
|
if (reverseVideoHere) {
|
||||||
|
int tmp = foreColor;
|
||||||
|
foreColor = backColor;
|
||||||
|
backColor = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
float left = startColumn * mFontWidth;
|
||||||
|
float right = left + runWidthColumns * mFontWidth;
|
||||||
|
|
||||||
|
mes = mes / mFontWidth;
|
||||||
|
boolean savedMatrix = false;
|
||||||
|
if (Math.abs(mes - runWidthColumns) > 0.01) {
|
||||||
|
canvas.save();
|
||||||
|
canvas.scale(runWidthColumns / mes, 1.f);
|
||||||
|
left *= mes / runWidthColumns;
|
||||||
|
right *= mes / runWidthColumns;
|
||||||
|
savedMatrix = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
|
||||||
|
// Only draw non-default background.
|
||||||
|
mTextPaint.setColor(backColor);
|
||||||
|
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor != 0) {
|
||||||
|
mTextPaint.setColor(cursor);
|
||||||
|
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
|
||||||
|
if (cursorStyle == TerminalEmulator.CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
|
||||||
|
else if (cursorStyle == TerminalEmulator.CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
|
||||||
|
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
||||||
|
if (dim) {
|
||||||
|
int red = (0xFF & (foreColor >> 16));
|
||||||
|
int green = (0xFF & (foreColor >> 8));
|
||||||
|
int blue = (0xFF & foreColor);
|
||||||
|
// Dim color handling used by libvte which in turn took it from xterm
|
||||||
|
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
|
||||||
|
red = red * 2 / 3;
|
||||||
|
green = green * 2 / 3;
|
||||||
|
blue = blue * 2 / 3;
|
||||||
|
foreColor = 0xFF000000 + (red << 16) + (green << 8) + blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mTextPaint.setFakeBoldText(bold);
|
||||||
|
mTextPaint.setUnderlineText(underline);
|
||||||
|
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
|
||||||
|
mTextPaint.setStrikeThruText(strikeThrough);
|
||||||
|
mTextPaint.setColor(foreColor);
|
||||||
|
|
||||||
|
// The text alignment is the default Paint.Align.LEFT.
|
||||||
|
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedMatrix) canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
919
app/src/main/java/io/neoterm/view/TerminalView.java
Executable file
@ -0,0 +1,919 @@
|
|||||||
|
package io.neoterm.view;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyCharacterMap;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.BaseInputConnection;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
import android.widget.Scroller;
|
||||||
|
|
||||||
|
import io.neoterm.R;
|
||||||
|
import io.neoterm.terminal.EmulatorDebug;
|
||||||
|
import io.neoterm.terminal.KeyHandler;
|
||||||
|
import io.neoterm.terminal.TerminalBuffer;
|
||||||
|
import io.neoterm.terminal.TerminalEmulator;
|
||||||
|
import io.neoterm.terminal.TerminalSession;
|
||||||
|
|
||||||
|
/** View displaying and interacting with a {@link TerminalSession}. */
|
||||||
|
public final class TerminalView extends View {
|
||||||
|
|
||||||
|
/** Log view key and IME events. */
|
||||||
|
private static final boolean LOG_KEY_EVENTS = false;
|
||||||
|
|
||||||
|
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
|
||||||
|
TerminalSession mTermSession;
|
||||||
|
/** Our terminal emulator whose session is {@link #mTermSession}. */
|
||||||
|
TerminalEmulator mEmulator;
|
||||||
|
|
||||||
|
TerminalRenderer mRenderer;
|
||||||
|
|
||||||
|
TerminalViewClient mClient;
|
||||||
|
|
||||||
|
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
|
||||||
|
int mTopRow;
|
||||||
|
|
||||||
|
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
||||||
|
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||||
|
float mSelectionDownX, mSelectionDownY;
|
||||||
|
private ActionMode mActionMode;
|
||||||
|
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
||||||
|
|
||||||
|
float mScaleFactor = 1.f;
|
||||||
|
final GestureAndScaleRecognizer mGestureRecognizer;
|
||||||
|
|
||||||
|
/** Keep track of where mouse touch event started which we report as mouse scroll. */
|
||||||
|
private int mMouseScrollStartX = -1, mMouseScrollStartY = -1;
|
||||||
|
/** Keep track of the time when a touch event leading to sending mouse scroll events started. */
|
||||||
|
private long mMouseStartDownTime = -1;
|
||||||
|
|
||||||
|
final Scroller mScroller;
|
||||||
|
|
||||||
|
/** What was left in from scrolling movement. */
|
||||||
|
float mScrollRemainder;
|
||||||
|
|
||||||
|
/** If non-zero, this is the last unicode code point received if that was a combining character. */
|
||||||
|
int mCombiningAccent;
|
||||||
|
int mTextSize;
|
||||||
|
|
||||||
|
public TerminalView(Context context, AttributeSet attributeSet) { // NO_UCD (unused code)
|
||||||
|
super(context, attributeSet);
|
||||||
|
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
|
||||||
|
|
||||||
|
boolean scrolledWithFinger;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUp(MotionEvent e) {
|
||||||
|
mScrollRemainder = 0.0f;
|
||||||
|
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText && !scrolledWithFinger) {
|
||||||
|
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
||||||
|
// for zooming.
|
||||||
|
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
||||||
|
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
scrolledWithFinger = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapUp(MotionEvent e) {
|
||||||
|
if (mEmulator == null) return true;
|
||||||
|
if (mIsSelectingText) {
|
||||||
|
toggleSelectingText(null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
requestFocus();
|
||||||
|
if (!mEmulator.isMouseTrackingActive()) {
|
||||||
|
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
|
mClient.onSingleTapUp(e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
|
||||||
|
if (mEmulator == null || mIsSelectingText) return true;
|
||||||
|
if (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
|
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
||||||
|
// This means that we never report moving with button press-events for touch input,
|
||||||
|
// since we cannot just start sending these events without a starting press event,
|
||||||
|
// which we do not do for touch input, only mouse in onTouchEvent().
|
||||||
|
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||||
|
} else {
|
||||||
|
scrolledWithFinger = true;
|
||||||
|
distanceY += mScrollRemainder;
|
||||||
|
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
|
||||||
|
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
|
||||||
|
doScroll(e, deltaRows);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onScale(float focusX, float focusY, float scale) {
|
||||||
|
if (mEmulator == null || mIsSelectingText) return true;
|
||||||
|
mScaleFactor *= scale;
|
||||||
|
mScaleFactor = mClient.onScale(mScaleFactor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
||||||
|
if (mEmulator == null || mIsSelectingText) return true;
|
||||||
|
// Do not start scrolling until last fling has been taken care of:
|
||||||
|
if (!mScroller.isFinished()) return true;
|
||||||
|
|
||||||
|
final boolean mouseTrackingAtStartOfFling = mEmulator.isMouseTrackingActive();
|
||||||
|
float SCALE = 0.25f;
|
||||||
|
if (mouseTrackingAtStartOfFling) {
|
||||||
|
mScroller.fling(0, 0, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.mRows / 2, mEmulator.mRows / 2);
|
||||||
|
} else {
|
||||||
|
mScroller.fling(0, mTopRow, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.getScreen().getActiveTranscriptRows(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(new Runnable() {
|
||||||
|
private int mLastY = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (mouseTrackingAtStartOfFling != mEmulator.isMouseTrackingActive()) {
|
||||||
|
mScroller.abortAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mScroller.isFinished()) return;
|
||||||
|
boolean more = mScroller.computeScrollOffset();
|
||||||
|
int newY = mScroller.getCurrY();
|
||||||
|
int diff = mouseTrackingAtStartOfFling ? (newY - mLastY) : (newY - mTopRow);
|
||||||
|
doScroll(e2, diff);
|
||||||
|
mLastY = newY;
|
||||||
|
if (more) post(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(float x, float y) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDoubleTap(MotionEvent e) {
|
||||||
|
// Do not treat is as a single confirmed tap - it may be followed by zoom.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongPress(MotionEvent e) {
|
||||||
|
if (mGestureRecognizer.isInProgress()) return;
|
||||||
|
if (mClient.onLongPress(e)) return;
|
||||||
|
if (!mIsSelectingText) {
|
||||||
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||||
|
toggleSelectingText(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mScroller = new Scroller(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onKeyListener Listener for all kinds of key events, both hardware and IME (which makes it different from that
|
||||||
|
* available with {@link View#setOnKeyListener(OnKeyListener)}.
|
||||||
|
*/
|
||||||
|
public void setOnKeyListener(TerminalViewClient onKeyListener) {
|
||||||
|
this.mClient = onKeyListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a {@link TerminalSession} to this view.
|
||||||
|
*
|
||||||
|
* @param session The {@link TerminalSession} this view will be displaying.
|
||||||
|
*/
|
||||||
|
public boolean attachSession(TerminalSession session) {
|
||||||
|
if (session == mTermSession) return false;
|
||||||
|
mTopRow = 0;
|
||||||
|
|
||||||
|
mTermSession = session;
|
||||||
|
mEmulator = null;
|
||||||
|
mCombiningAccent = 0;
|
||||||
|
|
||||||
|
updateSize();
|
||||||
|
|
||||||
|
// Wait with enabling the scrollbar until we have a terminal to get scroll position from.
|
||||||
|
setVerticalScrollBarEnabled(true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||||
|
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
|
||||||
|
//
|
||||||
|
// Previous keyboard issues:
|
||||||
|
// https://github.com/termux/termux-packages/issues/25
|
||||||
|
// https://github.com/termux/termux-app/issues/87.
|
||||||
|
// https://github.com/termux/termux-app/issues/126.
|
||||||
|
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
|
||||||
|
outAttrs.inputType = InputType.TYPE_NULL;
|
||||||
|
|
||||||
|
// Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen
|
||||||
|
// keyboard on Android TV (see https://github.com/termux/termux-app/issues/221).
|
||||||
|
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||||
|
|
||||||
|
return new BaseInputConnection(this, true) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean finishComposingText() {
|
||||||
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
|
||||||
|
super.finishComposingText();
|
||||||
|
|
||||||
|
sendTextToTerminal(getEditable());
|
||||||
|
getEditable().clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||||
|
if (LOG_KEY_EVENTS) {
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
|
||||||
|
}
|
||||||
|
super.commitText(text, newCursorPosition);
|
||||||
|
|
||||||
|
if (mEmulator == null) return true;
|
||||||
|
|
||||||
|
Editable content = getEditable();
|
||||||
|
sendTextToTerminal(content);
|
||||||
|
content.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
||||||
|
if (LOG_KEY_EVENTS) {
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
|
||||||
|
}
|
||||||
|
// The stock Samsung keyboard with 'Auto check spelling' enabled sends leftLength > 1.
|
||||||
|
KeyEvent deleteKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
|
||||||
|
for (int i = 0; i < leftLength; i++) sendKeyEvent(deleteKey);
|
||||||
|
return super.deleteSurroundingText(leftLength, rightLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendTextToTerminal(CharSequence text) {
|
||||||
|
final int textLengthInChars = text.length();
|
||||||
|
for (int i = 0; i < textLengthInChars; i++) {
|
||||||
|
char firstChar = text.charAt(i);
|
||||||
|
int codePoint;
|
||||||
|
if (Character.isHighSurrogate(firstChar)) {
|
||||||
|
if (++i < textLengthInChars) {
|
||||||
|
codePoint = Character.toCodePoint(firstChar, text.charAt(i));
|
||||||
|
} else {
|
||||||
|
// At end of string, with no low surrogate following the high:
|
||||||
|
codePoint = TerminalEmulator.UNICODE_REPLACEMENT_CHAR;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
codePoint = firstChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean ctrlHeld = false;
|
||||||
|
if (codePoint <= 31 && codePoint != 27) {
|
||||||
|
if (codePoint == '\n') {
|
||||||
|
// The AOSP keyboard and descendants seems to send \n as text when the enter key is pressed,
|
||||||
|
// instead of a key event like most other keyboard apps. A terminal expects \r for the enter
|
||||||
|
// key (although when icrnl is enabled this doesn't make a difference - run 'stty -icrnl' to
|
||||||
|
// check the behaviour).
|
||||||
|
codePoint = '\r';
|
||||||
|
}
|
||||||
|
|
||||||
|
// E.g. penti keyboard for ctrl input.
|
||||||
|
ctrlHeld = true;
|
||||||
|
switch (codePoint) {
|
||||||
|
case 31:
|
||||||
|
codePoint = '_';
|
||||||
|
break;
|
||||||
|
case 30:
|
||||||
|
codePoint = '^';
|
||||||
|
break;
|
||||||
|
case 29:
|
||||||
|
codePoint = ']';
|
||||||
|
break;
|
||||||
|
case 28:
|
||||||
|
codePoint = '\\';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
codePoint += 96;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputCodePoint(codePoint, ctrlHeld, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int computeVerticalScrollRange() {
|
||||||
|
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int computeVerticalScrollExtent() {
|
||||||
|
return mEmulator == null ? 1 : mEmulator.mRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int computeVerticalScrollOffset() {
|
||||||
|
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onScreenUpdated() {
|
||||||
|
if (mEmulator == null) return;
|
||||||
|
|
||||||
|
boolean skipScrolling = false;
|
||||||
|
if (mIsSelectingText) {
|
||||||
|
// Do not scroll when selecting text.
|
||||||
|
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||||
|
int rowShift = mEmulator.getScrollCounter();
|
||||||
|
if (-mTopRow + rowShift > rowsInHistory) {
|
||||||
|
// .. unless we're hitting the end of history transcript, in which
|
||||||
|
// case we abort text selection and scroll to end.
|
||||||
|
toggleSelectingText(null);
|
||||||
|
} else {
|
||||||
|
skipScrolling = true;
|
||||||
|
mTopRow -= rowShift;
|
||||||
|
mSelY1 -= rowShift;
|
||||||
|
mSelY2 -= rowShift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipScrolling && mTopRow != 0) {
|
||||||
|
// Scroll down if not already there.
|
||||||
|
if (mTopRow < -3) {
|
||||||
|
// Awaken scroll bars only if scrolling a noticeable amount
|
||||||
|
// - we do not want visible scroll bars during normal typing
|
||||||
|
// of one row at a time.
|
||||||
|
awakenScrollBars();
|
||||||
|
}
|
||||||
|
mTopRow = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mEmulator.clearScrollCounter();
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTextSize() {
|
||||||
|
return mTextSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text size, which in turn sets the number of rows and columns.
|
||||||
|
*
|
||||||
|
* @param textSize the new font size, in density-independent pixels.
|
||||||
|
*/
|
||||||
|
public void setTextSize(int textSize) {
|
||||||
|
this.mTextSize = textSize;
|
||||||
|
mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface);
|
||||||
|
updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTypeface(Typeface newTypeface) {
|
||||||
|
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
||||||
|
updateSize();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCheckIsTextEditor() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOpaque() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send a single mouse event code to the terminal. */
|
||||||
|
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
|
||||||
|
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
|
||||||
|
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
|
||||||
|
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
|
||||||
|
if (mMouseStartDownTime == e.getDownTime()) {
|
||||||
|
x = mMouseScrollStartX;
|
||||||
|
y = mMouseScrollStartY;
|
||||||
|
} else {
|
||||||
|
mMouseStartDownTime = e.getDownTime();
|
||||||
|
mMouseScrollStartX = x;
|
||||||
|
mMouseScrollStartY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mEmulator.sendMouseEvent(button, x, y, pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Perform a scroll, either from dragging the screen or by scrolling a mouse wheel. */
|
||||||
|
void doScroll(MotionEvent event, int rowsDown) {
|
||||||
|
boolean up = rowsDown < 0;
|
||||||
|
int amount = Math.abs(rowsDown);
|
||||||
|
for (int i = 0; i < amount; i++) {
|
||||||
|
if (mEmulator.isMouseTrackingActive()) {
|
||||||
|
sendMouseEventCode(event, up ? TerminalEmulator.MOUSE_WHEELUP_BUTTON : TerminalEmulator.MOUSE_WHEELDOWN_BUTTON, true);
|
||||||
|
} else if (mEmulator.isAlternateBufferActive()) {
|
||||||
|
// Send up and down key events for scrolling, which is what some terminals do to make scroll work in
|
||||||
|
// e.g. less, which shifts to the alt screen without mouse handling.
|
||||||
|
handleKeyCode(up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN, 0);
|
||||||
|
} else {
|
||||||
|
mTopRow = Math.min(0, Math.max(-(mEmulator.getScreen().getActiveTranscriptRows()), mTopRow + (up ? -1 : 1)));
|
||||||
|
if (!awakenScrollBars()) invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Overriding {@link View#onGenericMotionEvent(MotionEvent)}. */
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||||
|
if (mEmulator != null && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) {
|
||||||
|
// Handle mouse wheel scrolling.
|
||||||
|
boolean up = event.getAxisValue(MotionEvent.AXIS_VSCROLL) > 0.0f;
|
||||||
|
doScroll(event, up ? -3 : 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@Override
|
||||||
|
@TargetApi(23)
|
||||||
|
public boolean onTouchEvent(MotionEvent ev) {
|
||||||
|
if (mEmulator == null) return true;
|
||||||
|
final int action = ev.getAction();
|
||||||
|
|
||||||
|
if (mIsSelectingText) {
|
||||||
|
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
||||||
|
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
mInitialTextSelection = false;
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
int distanceFromSel1 = Math.abs(cx - mSelX1) + Math.abs(cy - mSelY1);
|
||||||
|
int distanceFromSel2 = Math.abs(cx - mSelX2) + Math.abs(cy - mSelY2);
|
||||||
|
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
|
||||||
|
mSelectionDownX = ev.getX();
|
||||||
|
mSelectionDownY = ev.getY();
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (mInitialTextSelection) break;
|
||||||
|
float deltaX = ev.getX() - mSelectionDownX;
|
||||||
|
float deltaY = ev.getY() - mSelectionDownY;
|
||||||
|
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
|
||||||
|
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
|
||||||
|
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
|
||||||
|
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
|
||||||
|
if (mIsDraggingLeftSelection) {
|
||||||
|
mSelX1 += deltaCols;
|
||||||
|
mSelY1 += deltaRows;
|
||||||
|
} else {
|
||||||
|
mSelX2 += deltaCols;
|
||||||
|
mSelY2 += deltaRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
|
||||||
|
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
|
||||||
|
|
||||||
|
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
|
||||||
|
// Switch handles.
|
||||||
|
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
|
||||||
|
int tmpX1 = mSelX1, tmpY1 = mSelY1;
|
||||||
|
mSelX1 = mSelX2;
|
||||||
|
mSelY1 = mSelY2;
|
||||||
|
mSelX2 = tmpX1;
|
||||||
|
mSelY2 = tmpY1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||||
|
mActionMode.invalidateContentRect();
|
||||||
|
invalidate();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mGestureRecognizer.onTouchEvent(ev);
|
||||||
|
return true;
|
||||||
|
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
|
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||||
|
return true;
|
||||||
|
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clipData = clipboard.getPrimaryClip();
|
||||||
|
if (clipData != null) {
|
||||||
|
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||||
|
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||||
|
}
|
||||||
|
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
||||||
|
switch (ev.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mGestureRecognizer.onTouchEvent(ev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||||
|
if (LOG_KEY_EVENTS)
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
if (mIsSelectingText) {
|
||||||
|
toggleSelectingText(null);
|
||||||
|
return true;
|
||||||
|
} else if (mClient.shouldBackButtonBeMappedToEscape()) {
|
||||||
|
// Intercept back button to treat it as escape:
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case KeyEvent.ACTION_DOWN:
|
||||||
|
return onKeyDown(keyCode, event);
|
||||||
|
case KeyEvent.ACTION_UP:
|
||||||
|
return onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyPreIme(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
|
if (LOG_KEY_EVENTS)
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
|
||||||
|
if (mEmulator == null) return true;
|
||||||
|
|
||||||
|
if (mClient.onKeyDown(keyCode, event, mTermSession)) {
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
} else if (event.isSystem() && (!mClient.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
|
||||||
|
return super.onKeyDown(keyCode, event);
|
||||||
|
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
||||||
|
mTermSession.write(event.getCharacters());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int metaState = event.getMetaState();
|
||||||
|
final boolean controlDownFromEvent = event.isCtrlPressed();
|
||||||
|
final boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
|
||||||
|
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
||||||
|
|
||||||
|
int keyMod = 0;
|
||||||
|
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
|
||||||
|
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
|
||||||
|
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
||||||
|
if (handleKeyCode(keyCode, keyMod)) {
|
||||||
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear Ctrl since we handle that ourselves:
|
||||||
|
int bitsToClear = KeyEvent.META_CTRL_MASK;
|
||||||
|
if (rightAltDownFromEvent) {
|
||||||
|
// Let right Alt/Alt Gr be used to compose characters.
|
||||||
|
} else {
|
||||||
|
// Use left alt to send to terminal (e.g. Left Alt+B to jump back a word), so remove:
|
||||||
|
bitsToClear |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||||
|
}
|
||||||
|
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
|
||||||
|
|
||||||
|
int result = event.getUnicodeChar(effectiveMetaState);
|
||||||
|
if (LOG_KEY_EVENTS)
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
|
||||||
|
if (result == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oldCombiningAccent = mCombiningAccent;
|
||||||
|
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
||||||
|
// If entered combining accent previously, write it out:
|
||||||
|
if (mCombiningAccent != 0)
|
||||||
|
inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
|
||||||
|
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
||||||
|
} else {
|
||||||
|
if (mCombiningAccent != 0) {
|
||||||
|
int combinedChar = KeyCharacterMap.getDeadChar(mCombiningAccent, result);
|
||||||
|
if (combinedChar > 0) result = combinedChar;
|
||||||
|
mCombiningAccent = 0;
|
||||||
|
}
|
||||||
|
inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||||
|
if (LOG_KEY_EVENTS) {
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||||
|
+ leftAltDownFromEvent + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
|
||||||
|
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();
|
||||||
|
|
||||||
|
if (mClient.onCodePoint(codePoint, controlDown, mTermSession)) return;
|
||||||
|
|
||||||
|
if (controlDown) {
|
||||||
|
if (codePoint >= 'a' && codePoint <= 'z') {
|
||||||
|
codePoint = codePoint - 'a' + 1;
|
||||||
|
} else if (codePoint >= 'A' && codePoint <= 'Z') {
|
||||||
|
codePoint = codePoint - 'A' + 1;
|
||||||
|
} else if (codePoint == ' ' || codePoint == '2') {
|
||||||
|
codePoint = 0;
|
||||||
|
} else if (codePoint == '[' || codePoint == '3') {
|
||||||
|
codePoint = 27; // ^[ (Esc)
|
||||||
|
} else if (codePoint == '\\' || codePoint == '4') {
|
||||||
|
codePoint = 28;
|
||||||
|
} else if (codePoint == ']' || codePoint == '5') {
|
||||||
|
codePoint = 29;
|
||||||
|
} else if (codePoint == '^' || codePoint == '6') {
|
||||||
|
codePoint = 30; // control-^
|
||||||
|
} else if (codePoint == '_' || codePoint == '7' || codePoint == '/') {
|
||||||
|
// "Ctrl-/ sends 0x1f which is equivalent of Ctrl-_ since the days of VT102"
|
||||||
|
// - http://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||||
|
codePoint = 31;
|
||||||
|
} else if (codePoint == '8') {
|
||||||
|
codePoint = 127; // DEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codePoint > -1) {
|
||||||
|
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||||
|
// of the more normal ones from ASCII that terminal programs expect - the
|
||||||
|
// desire to input the original characters should be low.
|
||||||
|
switch (codePoint) {
|
||||||
|
case 0x02DC: // SMALL TILDE.
|
||||||
|
codePoint = 0x007E; // TILDE (~).
|
||||||
|
break;
|
||||||
|
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
||||||
|
codePoint = 0x0060; // GRAVE ACCENT (`).
|
||||||
|
break;
|
||||||
|
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
||||||
|
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
||||||
|
mTermSession.writeCodePoint(altDown, codePoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Input the specified keyCode if applicable and return if the input was consumed. */
|
||||||
|
public boolean handleKeyCode(int keyCode, int keyMod) {
|
||||||
|
TerminalEmulator term = mTermSession.getEmulator();
|
||||||
|
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
|
||||||
|
if (code == null) return false;
|
||||||
|
mTermSession.write(code);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a key is released in the view.
|
||||||
|
*
|
||||||
|
* @param keyCode The keycode of the key which was released.
|
||||||
|
* @param event A {@link KeyEvent} describing the event.
|
||||||
|
* @return Whether the event was handled.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
|
if (LOG_KEY_EVENTS)
|
||||||
|
Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
||||||
|
if (mEmulator == null) return true;
|
||||||
|
|
||||||
|
if (mClient.onKeyUp(keyCode, event)) {
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
} else if (event.isSystem()) {
|
||||||
|
// Let system key events through.
|
||||||
|
return super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called during layout when the size of this view has changed. If you were just added to the view
|
||||||
|
* hierarchy, you're called with the old values of 0.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if the terminal size in rows and columns should be updated. */
|
||||||
|
public void updateSize() {
|
||||||
|
int viewWidth = getWidth();
|
||||||
|
int viewHeight = getHeight();
|
||||||
|
if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return;
|
||||||
|
|
||||||
|
// Set to 80 and 24 if you want to enable vttest.
|
||||||
|
int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth));
|
||||||
|
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
|
||||||
|
|
||||||
|
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
||||||
|
mTermSession.updateSize(newColumns, newRows);
|
||||||
|
mEmulator = mTermSession.getEmulator();
|
||||||
|
|
||||||
|
mTopRow = 0;
|
||||||
|
scrollTo(0, 0);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
if (mEmulator == null) {
|
||||||
|
canvas.drawColor(0XFF000000);
|
||||||
|
} else {
|
||||||
|
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
||||||
|
|
||||||
|
if (mIsSelectingText) {
|
||||||
|
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
|
||||||
|
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
|
||||||
|
|
||||||
|
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
|
||||||
|
int top = (mSelY1 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||||
|
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
|
||||||
|
mLeftSelectionHandle.draw(canvas);
|
||||||
|
|
||||||
|
int left = Math.round((mSelX2 + 1) * mRenderer.mFontWidth) - gripHandleMargin;
|
||||||
|
top = (mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||||
|
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
|
||||||
|
mRightSelectionHandle.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle text selection mode in the view. */
|
||||||
|
@TargetApi(23)
|
||||||
|
public void toggleSelectingText(MotionEvent ev) {
|
||||||
|
mIsSelectingText = !mIsSelectingText;
|
||||||
|
mClient.copyModeChanged(mIsSelectingText);
|
||||||
|
|
||||||
|
if (mIsSelectingText) {
|
||||||
|
if (mLeftSelectionHandle == null) {
|
||||||
|
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
||||||
|
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||||
|
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||||
|
// Offset for finger:
|
||||||
|
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||||
|
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
||||||
|
|
||||||
|
mSelX1 = mSelX2 = cx;
|
||||||
|
mSelY1 = mSelY2 = cy;
|
||||||
|
|
||||||
|
TerminalBuffer screen = mEmulator.getScreen();
|
||||||
|
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
||||||
|
// Selecting something other than whitespace. Expand to word.
|
||||||
|
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) {
|
||||||
|
mSelX1--;
|
||||||
|
}
|
||||||
|
while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
|
||||||
|
mSelX2++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mInitialTextSelection = true;
|
||||||
|
mIsDraggingLeftSelection = true;
|
||||||
|
mSelectionDownX = ev.getX();
|
||||||
|
mSelectionDownY = ev.getY();
|
||||||
|
|
||||||
|
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||||
|
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setShowAsAction(show);
|
||||||
|
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||||
|
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case 1:
|
||||||
|
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||||
|
mTermSession.clipboardText(selectedText);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clipData = clipboard.getPrimaryClip();
|
||||||
|
if (clipData != null) {
|
||||||
|
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||||
|
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
showContextMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
toggleSelectingText(null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
mActionMode = startActionMode(new ActionMode.Callback2() {
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return callback.onCreateActionMode(mode, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
return callback.onActionItemClicked(mode, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
||||||
|
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
||||||
|
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
||||||
|
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||||
|
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||||
|
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
|
||||||
|
}
|
||||||
|
}, ActionMode.TYPE_FLOATING);
|
||||||
|
} else {
|
||||||
|
mActionMode = startActionMode(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
} else {
|
||||||
|
mActionMode.finish();
|
||||||
|
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TerminalSession getCurrentSession() {
|
||||||
|
return mTermSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
app/src/main/java/io/neoterm/view/TerminalViewClient.java
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
package io.neoterm.view;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.ScaleGestureDetector;
|
||||||
|
|
||||||
|
import io.neoterm.terminal.TerminalSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input and scale listener which may be set on a {@link TerminalView} through
|
||||||
|
* {@link TerminalView#setOnKeyListener(TerminalViewClient)}.
|
||||||
|
* <p/>
|
||||||
|
*/
|
||||||
|
public interface TerminalViewClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}.
|
||||||
|
*/
|
||||||
|
float onScale(float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On a single tap on the terminal if terminal mouse reporting not enabled.
|
||||||
|
*/
|
||||||
|
void onSingleTapUp(MotionEvent e);
|
||||||
|
|
||||||
|
boolean shouldBackButtonBeMappedToEscape();
|
||||||
|
|
||||||
|
void copyModeChanged(boolean copyMode);
|
||||||
|
|
||||||
|
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);
|
||||||
|
|
||||||
|
boolean onKeyUp(int keyCode, KeyEvent e);
|
||||||
|
|
||||||
|
boolean readControlKey();
|
||||||
|
|
||||||
|
boolean readAltKey();
|
||||||
|
|
||||||
|
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||||
|
|
||||||
|
boolean onLongPress(MotionEvent event);
|
||||||
|
|
||||||
|
}
|
BIN
app/src/main/res/drawable-hdpi/ic_add_box_white_24dp.png
Executable file
After Width: | Height: | Size: 280 B |
BIN
app/src/main/res/drawable-mdpi/ic_add_box_white_24dp.png
Executable file
After Width: | Height: | Size: 210 B |
BIN
app/src/main/res/drawable-xhdpi/ic_add_box_white_24dp.png
Executable file
After Width: | Height: | Size: 257 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_add_box_white_24dp.png
Executable file
After Width: | Height: | Size: 324 B |
BIN
app/src/main/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png
Executable file
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_add_box_white_24dp.png
Executable file
After Width: | Height: | Size: 486 B |
4
app/src/main/res/drawable/text_select_handle_left_material.xml
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
||||||
|
android:tint="#2196F3" />
|
4
app/src/main/res/drawable/text_select_handle_right_material.xml
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
||||||
|
android:tint="#2196F3" />
|
27
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/black"
|
||||||
|
tools:context="io.neoterm.MainActivity">
|
||||||
|
|
||||||
|
<io.neoterm.view.ExtraKeysView
|
||||||
|
android:id="@+id/extra_keys"
|
||||||
|
style="?android:buttonBarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:orientation="horizontal" />
|
||||||
|
|
||||||
|
<io.neoterm.view.TerminalView
|
||||||
|
android:id="@+id/terminal_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_above="@id/extra_keys"
|
||||||
|
android:fadeScrollbars="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
14
app/src/main/res/layout/tab_main.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<de.mrapp.android.tabswitcher.TabSwitcher
|
||||||
|
android:id="@+id/tab_switcher"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:custom="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#ff14181c"
|
||||||
|
custom:layoutPolicy="auto"
|
||||||
|
custom:tabBackgroundColor="@color/tab_background_color"
|
||||||
|
custom:tabTitleTextColor="@color/tab_title_text_color"
|
||||||
|
custom:tabCloseButtonIcon="@drawable/ic_close_tab_18dp"
|
||||||
|
custom:toolbarMenu="@menu/tab_switcher"
|
||||||
|
custom:toolbarNavigationIcon="@drawable/ic_add_box_white_24dp"/>
|
30
app/src/main/res/menu/tab_switcher.xml
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2016 - 2017 Michael Rapp
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
compliance with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
License.
|
||||||
|
-->
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/toggle_tab_switcher_menu_item"
|
||||||
|
android:title="@string/toggle_tab_switcher_menu_item"
|
||||||
|
app:actionLayout="@layout/tab_switcher_menu_item"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/add_tab_menu_item"
|
||||||
|
android:title="@string/add_tab_menu_item"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
</menu>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
6
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
</resources>
|
10
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Neo Term</string>
|
||||||
|
<string name="copy_text">Copy</string>
|
||||||
|
<string name="paste_text">Paste</string>
|
||||||
|
<string name="text_selection_more">More</string>
|
||||||
|
|
||||||
|
<string name="toggle_tab_switcher_menu_item">Toggle switcher</string>
|
||||||
|
<string name="add_tab_menu_item">New tab</string>
|
||||||
|
<string name="clear_tabs_menu_item">Clear all tabs</string>
|
||||||
|
</resources>
|
8
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
18
app/src/test/java/io/neoterm/ExampleUnitTest.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package io.neoterm
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see [Testing documentation](http://d.android.com/tools/testing)
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, (2 + 2).toLong())
|
||||||
|
}
|
||||||
|
}
|
28
build.gradle
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.1.2-4'
|
||||||
|
repositories {
|
||||||
|
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.0.0-alpha1'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
1
chrome-tabs/.gitignore
vendored
Executable file
@ -0,0 +1 @@
|
|||||||
|
/build
|
25
chrome-tabs/build.gradle
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 25
|
||||||
|
buildToolsVersion "25.0.2"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 25
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'com.github.michael-rapp:android-util:1.15.0'
|
||||||
|
compile 'com.android.support:support-annotations:25.3.0'
|
||||||
|
compile 'com.android.support:appcompat-v7:25.3.0'
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
}
|
3
chrome-tabs/gradle.properties
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
POM_NAME=ChromeLikeTabSwitcher
|
||||||
|
POM_ARTIFACT_ID=chrome-like-tab-switcher
|
||||||
|
POM_PACKAGING=aar
|
16
chrome-tabs/src/main/AndroidManifest.xml
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2016 - 2017 Michael Rapp
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
compliance with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
License.
|
||||||
|
-->
|
||||||
|
<manifest package="de.mrapp.android.tabswitcher"/>
|
162
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/Animation.java
Executable file
@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureAtLeast;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An animation, which can be used to add or remove tabs to/from a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public abstract class Animation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for all builders, which allow to configure and create instances of the
|
||||||
|
* class {@link Animation}.
|
||||||
|
*
|
||||||
|
* @param <AnimationType>
|
||||||
|
* The type of the animations, which are created by the builder
|
||||||
|
* @param <BuilderType>
|
||||||
|
* The type of the builder
|
||||||
|
*/
|
||||||
|
protected static abstract class Builder<AnimationType, BuilderType> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration of the animations, which are created by the builder.
|
||||||
|
*/
|
||||||
|
protected long duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interpolator, which is used by the animations, which are created by the builder.
|
||||||
|
*/
|
||||||
|
protected Interpolator interpolator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reference to the builder.
|
||||||
|
*
|
||||||
|
* @return A reference to the builder, casted to the generic type BuilderType. The reference
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected final BuilderType self() {
|
||||||
|
return (BuilderType) this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* Animation}.
|
||||||
|
*/
|
||||||
|
public Builder() {
|
||||||
|
setDuration(-1);
|
||||||
|
setInterpolator(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns the animation.
|
||||||
|
*
|
||||||
|
* @return The animation, which has been created, as an instance of the generic type
|
||||||
|
* AnimationType. The animation may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public abstract AnimationType create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the duration of the animations, which are created by the builder.
|
||||||
|
*
|
||||||
|
* @param duration
|
||||||
|
* The duration, which should be set, in milliseconds as a {@link Long} value or -1,
|
||||||
|
* if the default duration should be used
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final BuilderType setDuration(final long duration) {
|
||||||
|
ensureAtLeast(duration, -1, "The duration must be at least -1");
|
||||||
|
this.duration = duration;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the interpolator, which should be used by the animations, which are created by the
|
||||||
|
* builder.
|
||||||
|
*
|
||||||
|
* @param interpolator
|
||||||
|
* The interpolator, which should be set, as an instance of the type {@link
|
||||||
|
* Interpolator} or null, if the default interpolator should be used
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final BuilderType setInterpolator(@Nullable final Interpolator interpolator) {
|
||||||
|
this.interpolator = interpolator;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration of the animation in milliseconds.
|
||||||
|
*/
|
||||||
|
private final long duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interpolator, which is used by the animation.
|
||||||
|
*/
|
||||||
|
private final Interpolator interpolator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new animation.
|
||||||
|
*
|
||||||
|
* @param duration
|
||||||
|
* The duration of the animation in milliseconds as a {@link Long} value or -1, if the
|
||||||
|
* default duration should be used
|
||||||
|
* @param interpolator
|
||||||
|
* The interpolator, which should be used by the animation, as an instance of the type
|
||||||
|
* {@link Interpolator} or null, if the default interpolator should be used
|
||||||
|
*/
|
||||||
|
protected Animation(final long duration, @Nullable final Interpolator interpolator) {
|
||||||
|
ensureAtLeast(duration, -1, "The duration must be at least -1");
|
||||||
|
this.duration = duration;
|
||||||
|
this.interpolator = interpolator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the duration of the animation.
|
||||||
|
*
|
||||||
|
* @return The duration of the animation in milliseconds as a {@link Long} value or -1, if the
|
||||||
|
* default duration is used
|
||||||
|
*/
|
||||||
|
public final long getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the interpolator, which is used by the animation.
|
||||||
|
*
|
||||||
|
* @return The interpolator, which is used by the animation, as an instance of the type {@link
|
||||||
|
* Interpolator} or null, if the default interpolator is used
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final Interpolator getInterpolator() {
|
||||||
|
return interpolator;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/Layout.java
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all possible layouts of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public enum Layout {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout, which is used on smartphones and phablet devices, when in portrait mode.
|
||||||
|
*/
|
||||||
|
PHONE_PORTRAIT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout, which is used on smartphones and phablet devices, when in landscape mode.
|
||||||
|
*/
|
||||||
|
PHONE_LANDSCAPE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout, which is used on tablets.
|
||||||
|
*/
|
||||||
|
TABLET
|
||||||
|
|
||||||
|
}
|
83
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/LayoutPolicy.java
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all possible layout policies of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public enum LayoutPolicy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the layout should automatically adapted, depending on whether the device is a smartphone
|
||||||
|
* or tablet.
|
||||||
|
*/
|
||||||
|
AUTO(0),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the smartphone layout should be used, regardless of the device.
|
||||||
|
*/
|
||||||
|
PHONE(1),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the tablet layout should be used, regardless of the device.
|
||||||
|
*/
|
||||||
|
TABLET(2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the layout policy.
|
||||||
|
*/
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new layout policy.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The value of the layout policy as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
LayoutPolicy(final int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the layout policy.
|
||||||
|
*
|
||||||
|
* @return The value of the layout policy as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
public final int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the layout policy, which corresponds to a specific value.
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The value of the layout policy, which should be returned, as an {@link Integer}
|
||||||
|
* value
|
||||||
|
* @return The layout policy, which corresponds to the given value, as a value of the enum
|
||||||
|
* {@link LayoutPolicy}
|
||||||
|
*/
|
||||||
|
public static LayoutPolicy fromValue(final int value) {
|
||||||
|
for (LayoutPolicy layoutPolicy : values()) {
|
||||||
|
if (layoutPolicy.getValue() == value) {
|
||||||
|
return layoutPolicy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Invalid enum value: " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
140
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/PeekAnimation.java
Executable file
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A peek animation, which animates the size of a tab starting at a specific position in order to
|
||||||
|
* show the tab for a short time at the end of a {@link TabSwitcher}. Peek animations can be used to
|
||||||
|
* add tabs while the tab switcher is not shown and when using the smartphone layout. They are meant
|
||||||
|
* to be used when adding a tab without selecting it and enable the user to peek at the added tab
|
||||||
|
* for a short moment.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class PeekAnimation extends Animation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder, which allows to configure and create instances of the class {@link
|
||||||
|
* PeekAnimation}.
|
||||||
|
*/
|
||||||
|
public static class Builder extends Animation.Builder<PeekAnimation, Builder> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The horizontal position, the animations, which are created by the builder, start at.
|
||||||
|
*/
|
||||||
|
private float x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vertical position, the animations, which are created by the builder, start at.
|
||||||
|
*/
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* PeekAnimation}.
|
||||||
|
*/
|
||||||
|
public Builder() {
|
||||||
|
setX(0);
|
||||||
|
setY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the horizontal position, the animations, which are created by the builder, should
|
||||||
|
* start at.
|
||||||
|
*
|
||||||
|
* @param x
|
||||||
|
* The horizontal position, which should be set, in pixels as a {@link Float} value
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Builder setX(final float x) {
|
||||||
|
this.x = x;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the vertical position, the animations, which are created by the builder, should
|
||||||
|
* start at.
|
||||||
|
*
|
||||||
|
* @param y
|
||||||
|
* The vertical position, which should be set, in pixels as a {@link Float} value
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Builder setY(final float y) {
|
||||||
|
this.y = y;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final PeekAnimation create() {
|
||||||
|
return new PeekAnimation(duration, interpolator, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The horizontal position, the animation starts at.
|
||||||
|
*/
|
||||||
|
private final float x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vertical position, the animation starts at.
|
||||||
|
*/
|
||||||
|
private final float y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new reveal animation.
|
||||||
|
*
|
||||||
|
* @param x
|
||||||
|
* The horizontal position, the animation should start at, in pixels as a {@link Float}
|
||||||
|
* value
|
||||||
|
* @param y
|
||||||
|
* The vertical position, the animation should start at, in pixels as a {@link Float}
|
||||||
|
* value
|
||||||
|
*/
|
||||||
|
private PeekAnimation(final long duration, @Nullable final Interpolator interpolator,
|
||||||
|
final float x, final float y) {
|
||||||
|
super(duration, interpolator);
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the horizontal position, the animation starts at.
|
||||||
|
*
|
||||||
|
* @return The horizontal position, the animation starts at, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
public final float getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the vertical position, the animation starts at.
|
||||||
|
*
|
||||||
|
* @return The vertical position, the animation starts at, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
public final float getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
138
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/RevealAnimation.java
Executable file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reveal animation, which animates the size of a tab starting at a specific position. Reveal
|
||||||
|
* animations can be used to add tabs to a {@link TabSwitcher} when using the smartphone layout.
|
||||||
|
* Tabs, which have been added by using a reveal animation, are selected automatically.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class RevealAnimation extends Animation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder, which allows to configure and create instances of the class {@link
|
||||||
|
* RevealAnimation}.
|
||||||
|
*/
|
||||||
|
public static class Builder extends Animation.Builder<RevealAnimation, Builder> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The horizontal position, the animations, which are created by the builder, start at.
|
||||||
|
*/
|
||||||
|
private float x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vertical position, the animations, which are created by the builder, start at.
|
||||||
|
*/
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* RevealAnimation}.
|
||||||
|
*/
|
||||||
|
public Builder() {
|
||||||
|
setX(0);
|
||||||
|
setY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the horizontal position, the animations, which are created by the builder, should
|
||||||
|
* start at.
|
||||||
|
*
|
||||||
|
* @param x
|
||||||
|
* The horizontal position, which should be set, in pixels as a {@link Float} value
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Builder setX(final float x) {
|
||||||
|
this.x = x;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the vertical position, the animations, which are created by the builder, should
|
||||||
|
* start at.
|
||||||
|
*
|
||||||
|
* @param y
|
||||||
|
* The vertical position, which should be set, in pixels as a {@link Float} value
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Builder setY(final float y) {
|
||||||
|
this.y = y;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final RevealAnimation create() {
|
||||||
|
return new RevealAnimation(duration, interpolator, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The horizontal position, the animation starts at.
|
||||||
|
*/
|
||||||
|
private final float x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vertical position, the animation starts at.
|
||||||
|
*/
|
||||||
|
private final float y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new reveal animation.
|
||||||
|
*
|
||||||
|
* @param x
|
||||||
|
* The horizontal position, the animation should start at, in pixels as a {@link Float}
|
||||||
|
* value
|
||||||
|
* @param y
|
||||||
|
* The vertical position, the animation should start at, in pixels as a {@link Float}
|
||||||
|
* value
|
||||||
|
*/
|
||||||
|
private RevealAnimation(final long duration, @Nullable final Interpolator interpolator,
|
||||||
|
final float x, final float y) {
|
||||||
|
super(duration, interpolator);
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the horizontal position, the animation starts at.
|
||||||
|
*
|
||||||
|
* @return The horizontal position, the animation starts at, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
public final float getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the vertical position, the animation starts at.
|
||||||
|
*
|
||||||
|
* @return The vertical position, the animation starts at, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
public final float getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
131
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/SwipeAnimation.java
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A swipe animation, which moves tabs on the orthogonal axis, while animating their size and
|
||||||
|
* opacity at the same time. Swipe animations can be used to add or remove tabs to a {@link
|
||||||
|
* TabSwitcher} when using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class SwipeAnimation extends Animation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all possible directions of a swipe animation.
|
||||||
|
*/
|
||||||
|
public enum SwipeDirection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab should be swiped in/out from/to the left, respectively the top, when
|
||||||
|
* dragging horizontally.
|
||||||
|
*/
|
||||||
|
LEFT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab should be swiped in/out from/to the right, respectively the bottom, when
|
||||||
|
* dragging horizontally.
|
||||||
|
*/
|
||||||
|
RIGHT
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder, which allows to configure and create instances of the class {@link
|
||||||
|
* SwipeAnimation}.
|
||||||
|
*/
|
||||||
|
public static class Builder extends Animation.Builder<SwipeAnimation, Builder> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction of the animations, which are created by the builder.
|
||||||
|
*/
|
||||||
|
private SwipeDirection direction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* SwipeAnimation}.
|
||||||
|
*/
|
||||||
|
public Builder() {
|
||||||
|
setDirection(SwipeDirection.RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the direction of the animations, which are created by the builder.
|
||||||
|
*
|
||||||
|
* @param direction
|
||||||
|
* The direction, which should be set, as a value of the enum {@link
|
||||||
|
* SwipeDirection}. The direction may either be {@link SwipeDirection#LEFT} or
|
||||||
|
* {@link SwipeDirection#RIGHT}
|
||||||
|
* @return The builder, this method has be called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Builder setDirection(@NonNull final SwipeDirection direction) {
|
||||||
|
ensureNotNull(direction, "The direction may not be null");
|
||||||
|
this.direction = direction;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final SwipeAnimation create() {
|
||||||
|
return new SwipeAnimation(duration, interpolator, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction of the swipe animation.
|
||||||
|
*/
|
||||||
|
private final SwipeDirection direction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new swipe animation.
|
||||||
|
*
|
||||||
|
* @param duration
|
||||||
|
* The duration of the animation in milliseconds as a {@link Long} value or -1, if the
|
||||||
|
* default duration should be used
|
||||||
|
* @param interpolator
|
||||||
|
* The interpolator, which should be used by the animation, as an instance of the type
|
||||||
|
* {@link Interpolator} or null, if the default interpolator should be used
|
||||||
|
* @param direction
|
||||||
|
* The direction of the swipe animation as a value of the enum {@link SwipeDirection}.
|
||||||
|
* The direction may not be null
|
||||||
|
*/
|
||||||
|
private SwipeAnimation(final long duration, @Nullable final Interpolator interpolator,
|
||||||
|
@NonNull final SwipeDirection direction) {
|
||||||
|
super(duration, interpolator);
|
||||||
|
ensureNotNull(direction, "The direction may not be null");
|
||||||
|
this.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the direction of the swipe animation.
|
||||||
|
*
|
||||||
|
* @return The direction of the swipe animation as a value of the enum {@link SwipeDirection}.
|
||||||
|
* The direction may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final SwipeDirection getDirection() {
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
573
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/Tab.java
Executable file
@ -0,0 +1,573 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.ColorInt;
|
||||||
|
import android.support.annotation.DrawableRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotEmpty;
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tab, which can be added to a {@link TabSwitcher} widget. It has a title, as well as an optional
|
||||||
|
* icon. Furthermore, it is possible to set a custom color and to specify, whether the tab should be
|
||||||
|
* closeable, or not.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class Tab implements Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A creator, which allows to create instances of the class {@link Tab} from parcels.
|
||||||
|
*/
|
||||||
|
public static final Creator<Tab> CREATOR = new Creator<Tab>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tab createFromParcel(final Parcel source) {
|
||||||
|
return new Tab(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tab[] newArray(final int size) {
|
||||||
|
return new Tab[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified, when a tab's properties have been
|
||||||
|
* changed, must implement.
|
||||||
|
*/
|
||||||
|
public interface Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab's title has been changed.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The observed tab as an instance of the class {@link Tab}. The tab may not be
|
||||||
|
* null
|
||||||
|
*/
|
||||||
|
void onTitleChanged(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab's icon has been changed.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The observed tab as an instance of the class {@link Tab}. The tab may not be
|
||||||
|
* null
|
||||||
|
*/
|
||||||
|
void onIconChanged(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when it has been changed, whether the tab is closeable, or
|
||||||
|
* not.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The observed tab as an instance of the class {@link Tab}. The tab may not be
|
||||||
|
* null
|
||||||
|
*/
|
||||||
|
void onCloseableChanged(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the icon of the tab's close button has been changed.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The observed tab as an instance of the class {@link Tab}. The tab may not be
|
||||||
|
* null
|
||||||
|
*/
|
||||||
|
void onCloseButtonIconChanged(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab's background color has been changed.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The observed tab as an instance of the class {@link Tab}. The tab may not be
|
||||||
|
* null
|
||||||
|
*/
|
||||||
|
void onBackgroundColorChanged(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the text color of the tab's title has been changed.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The observed tab as an instance of the class {@link Tab}. The tab may not be
|
||||||
|
* null
|
||||||
|
*/
|
||||||
|
void onTitleTextColorChanged(@NonNull Tab tab);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set, which contains the callbacks, which have been registered to be notified, when the
|
||||||
|
* tab's properties have been changed.
|
||||||
|
*/
|
||||||
|
private final Set<Callback> callbacks = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab's title.
|
||||||
|
*/
|
||||||
|
private CharSequence title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource id of the tab's icon.
|
||||||
|
*/
|
||||||
|
private int iconId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab's icon as a bitmap.
|
||||||
|
*/
|
||||||
|
private Bitmap iconBitmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True, if the tab is closeable, false otherwise.
|
||||||
|
*/
|
||||||
|
private boolean closeable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource id of the icon of the tab's close button.
|
||||||
|
*/
|
||||||
|
private int closeButtonIconId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitmap of the icon of the tab's close button.
|
||||||
|
*/
|
||||||
|
private Bitmap closeButtonIconBitmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The background color of the tab.
|
||||||
|
*/
|
||||||
|
private ColorStateList backgroundColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text color of the tab's title.
|
||||||
|
*/
|
||||||
|
private ColorStateList titleTextColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional parameters, which are associated with the tab.
|
||||||
|
*/
|
||||||
|
private Bundle parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all callbacks, that the tab's title has been changed.
|
||||||
|
*/
|
||||||
|
private void notifyOnTitleChanged() {
|
||||||
|
for (Callback callback : callbacks) {
|
||||||
|
callback.onTitleChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all callbacks, that the tab's icon has been changed.
|
||||||
|
*/
|
||||||
|
private void notifyOnIconChanged() {
|
||||||
|
for (Callback callback : callbacks) {
|
||||||
|
callback.onIconChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all callbacks, that it has been changed, whether the tab is closeable, or not.
|
||||||
|
*/
|
||||||
|
private void notifyOnCloseableChanged() {
|
||||||
|
for (Callback callback : callbacks) {
|
||||||
|
callback.onCloseableChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all callbacks, that the icon of the tab's close button has been changed.
|
||||||
|
*/
|
||||||
|
private void notifyOnCloseButtonIconChanged() {
|
||||||
|
for (Callback callback : callbacks) {
|
||||||
|
callback.onCloseButtonIconChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all callbacks, that the background color of the tab has been changed.
|
||||||
|
*/
|
||||||
|
private void notifyOnBackgroundColorChanged() {
|
||||||
|
for (Callback callback : callbacks) {
|
||||||
|
callback.onBackgroundColorChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all callbacks, that the text color of the tab has been changed.
|
||||||
|
*/
|
||||||
|
private void notifyOnTitleTextColorChanged() {
|
||||||
|
for (Callback callback : callbacks) {
|
||||||
|
callback.onTitleTextColorChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tab, which can be added to a {@link TabSwitcher} widget.
|
||||||
|
*
|
||||||
|
* @param source
|
||||||
|
* The parcel, the tab should be created from, as an instance of the class {@link
|
||||||
|
* Parcel}. The parcel may not be null
|
||||||
|
*/
|
||||||
|
private Tab(@NonNull final Parcel source) {
|
||||||
|
this.title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
|
||||||
|
this.iconId = source.readInt();
|
||||||
|
this.iconBitmap = source.readParcelable(getClass().getClassLoader());
|
||||||
|
this.closeable = source.readInt() > 0;
|
||||||
|
this.closeButtonIconId = source.readInt();
|
||||||
|
this.closeButtonIconBitmap = source.readParcelable(getClass().getClassLoader());
|
||||||
|
this.backgroundColor = source.readParcelable(getClass().getClassLoader());
|
||||||
|
this.titleTextColor = source.readParcelable(getClass().getClassLoader());
|
||||||
|
this.parameters = source.readBundle(getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tab, which can be added to a {@link TabSwitcher} widget.
|
||||||
|
*
|
||||||
|
* @param title
|
||||||
|
* The tab's title as an instance of the type {@link CharSequence}. The title may not be
|
||||||
|
* neither be null, nor empty
|
||||||
|
*/
|
||||||
|
public Tab(@NonNull final CharSequence title) {
|
||||||
|
setTitle(title);
|
||||||
|
this.closeable = true;
|
||||||
|
this.closeButtonIconId = -1;
|
||||||
|
this.closeButtonIconBitmap = null;
|
||||||
|
this.iconId = -1;
|
||||||
|
this.iconBitmap = null;
|
||||||
|
this.backgroundColor = null;
|
||||||
|
this.titleTextColor = null;
|
||||||
|
this.parameters = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tab, which can be added to a {@link TabSwitcher} widget.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used, as an instance of the class {@link Context}. The
|
||||||
|
* context may not be null
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the tab's title as an {@link Integer} value. The resource id must
|
||||||
|
* correspond to a valid string resource
|
||||||
|
*/
|
||||||
|
public Tab(@NonNull final Context context, @StringRes final int resourceId) {
|
||||||
|
this(context.getString(resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab's title.
|
||||||
|
*
|
||||||
|
* @return The tab's title as an instance of the type {@link CharSequence}. The title may
|
||||||
|
* neither be null, nor empty
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final CharSequence getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tab's title.
|
||||||
|
*
|
||||||
|
* @param title
|
||||||
|
* The title, which should be set, as an instance of the type {@link CharSequence}. The
|
||||||
|
* title may neither be null, nor empty
|
||||||
|
*/
|
||||||
|
public final void setTitle(@NonNull final CharSequence title) {
|
||||||
|
ensureNotNull(title, "The title may not be null");
|
||||||
|
ensureNotEmpty(title, "The title may not be empty");
|
||||||
|
this.title = title;
|
||||||
|
notifyOnTitleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tab's title.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used, as an instance of the class {@link Context}. The
|
||||||
|
* context may not be null
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the title, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid string resource
|
||||||
|
*/
|
||||||
|
public final void setTitle(@NonNull final Context context, @StringRes final int resourceId) {
|
||||||
|
setTitle(context.getText(resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab's icon.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used, as an instance of the class {@link Context}. The
|
||||||
|
* context may not be null
|
||||||
|
* @return The tab's icon as an instance of the class {@link Drawable} or null, if no custom
|
||||||
|
* icon is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final Drawable getIcon(@NonNull final Context context) {
|
||||||
|
ensureNotNull(context, "The context may not be null");
|
||||||
|
|
||||||
|
if (iconId != -1) {
|
||||||
|
return ContextCompat.getDrawable(context, iconId);
|
||||||
|
} else {
|
||||||
|
return iconBitmap != null ? new BitmapDrawable(context.getResources(), iconBitmap) :
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tab's icon.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the icon, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid drawable resource
|
||||||
|
*/
|
||||||
|
public final void setIcon(@DrawableRes final int resourceId) {
|
||||||
|
this.iconId = resourceId;
|
||||||
|
this.iconBitmap = null;
|
||||||
|
notifyOnIconChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tab's icon.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which should be set, as an instance of the class {@link Bitmap} or null, if
|
||||||
|
* no custom icon should be set
|
||||||
|
*/
|
||||||
|
public final void setIcon(@Nullable final Bitmap icon) {
|
||||||
|
this.iconId = -1;
|
||||||
|
this.iconBitmap = icon;
|
||||||
|
notifyOnIconChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the tab is closeable, or not.
|
||||||
|
*
|
||||||
|
* @return True, if the tab is closeable, false otherwise
|
||||||
|
*/
|
||||||
|
public final boolean isCloseable() {
|
||||||
|
return closeable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets, whether the tab should be closeable, or not.
|
||||||
|
*
|
||||||
|
* @param closeable
|
||||||
|
* True, if the tab should be closeable, false otherwise
|
||||||
|
*/
|
||||||
|
public final void setCloseable(final boolean closeable) {
|
||||||
|
this.closeable = closeable;
|
||||||
|
notifyOnCloseableChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the icon of the tab's close button.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used to retrieve the icon, as an instance of the class
|
||||||
|
* {@link Context}. The context may not be null
|
||||||
|
* @return The icon of the tab's close button as an instance of the class {@link Drawable} or
|
||||||
|
* null, if no custom icon is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final Drawable getCloseButtonIcon(@NonNull final Context context) {
|
||||||
|
ensureNotNull(context, "The context may not be null");
|
||||||
|
|
||||||
|
if (closeButtonIconId != -1) {
|
||||||
|
return ContextCompat.getDrawable(context, closeButtonIconId);
|
||||||
|
} else {
|
||||||
|
return closeButtonIconBitmap != null ?
|
||||||
|
new BitmapDrawable(context.getResources(), closeButtonIconBitmap) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the icon of the tab's close button.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the icon, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid drawable resource
|
||||||
|
*/
|
||||||
|
public final void setCloseButtonIcon(@DrawableRes final int resourceId) {
|
||||||
|
this.closeButtonIconId = resourceId;
|
||||||
|
this.closeButtonIconBitmap = null;
|
||||||
|
notifyOnCloseButtonIconChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the icon of the tab's close button.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which should be set, as an instance of the class {@link Bitmap} or null, if
|
||||||
|
* no custom icon should be set
|
||||||
|
*/
|
||||||
|
public final void setCloseButtonIcon(@Nullable final Bitmap icon) {
|
||||||
|
this.closeButtonIconId = -1;
|
||||||
|
this.closeButtonIconBitmap = icon;
|
||||||
|
notifyOnCloseButtonIconChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the background color of the tab.
|
||||||
|
*
|
||||||
|
* @return The background color of the tab as an instance of the class {@link ColorStateList} or
|
||||||
|
* -1, if no custom color is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final ColorStateList getBackgroundColor() {
|
||||||
|
return backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tab's background color.
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* The color, which should be set, as an {@link Integer} value or -1, if no custom color
|
||||||
|
* should be set
|
||||||
|
*/
|
||||||
|
public final void setBackgroundColor(@ColorInt final int color) {
|
||||||
|
setBackgroundColor(color != -1 ? ColorStateList.valueOf(color) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tab's background color.
|
||||||
|
*
|
||||||
|
* @param colorStateList
|
||||||
|
* The color state list, which should be set, as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if no custom color should be set
|
||||||
|
*/
|
||||||
|
public final void setBackgroundColor(@Nullable final ColorStateList colorStateList) {
|
||||||
|
this.backgroundColor = colorStateList;
|
||||||
|
notifyOnBackgroundColorChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the text color of the tab's title.
|
||||||
|
*
|
||||||
|
* @return The text color of the tab's title as an instance of the class {@link ColorStateList}
|
||||||
|
* or null, if no custom color is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final ColorStateList getTitleTextColor() {
|
||||||
|
return titleTextColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text color of the tab's title.
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* The color, which should be set, as an {@link Integer} value or -1, if no custom color
|
||||||
|
* should be set
|
||||||
|
*/
|
||||||
|
public final void setTitleTextColor(@ColorInt final int color) {
|
||||||
|
setTitleTextColor(color != -1 ? ColorStateList.valueOf(color) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text color of the tab's title.
|
||||||
|
*
|
||||||
|
* @param colorStateList
|
||||||
|
* The color state list, which should be set, as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if no custom color should be set
|
||||||
|
*/
|
||||||
|
public final void setTitleTextColor(@Nullable final ColorStateList colorStateList) {
|
||||||
|
this.titleTextColor = colorStateList;
|
||||||
|
notifyOnTitleTextColorChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a bundle, which contains the optional parameters, which are associated with the tab.
|
||||||
|
*
|
||||||
|
* @return A bundle, which contains the optional parameters, which are associated with the tab,
|
||||||
|
* as an instance of the class {@link Bundle} or null, if no parameters are associated with the
|
||||||
|
* tab
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final Bundle getParameters() {
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a bundle, which contains the optional parameters, which should be associated with the
|
||||||
|
* tab.
|
||||||
|
*
|
||||||
|
* @param parameters
|
||||||
|
* The bundle, which should be set, as an instance of the class {@link Bundle} or null,
|
||||||
|
* if no parameters should be associated with the tab
|
||||||
|
*/
|
||||||
|
public final void setParameters(@Nullable final Bundle parameters) {
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new callback, which should be notified, when the tab's properties have been changed.
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
* The callback, which should be added, as an instance of the type {@link Callback}. The
|
||||||
|
* callback may not be null
|
||||||
|
*/
|
||||||
|
public final void addCallback(@NonNull final Callback callback) {
|
||||||
|
ensureNotNull(callback, "The callback may not be null");
|
||||||
|
this.callbacks.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a specific callback, which should not be notified, when the tab's properties have
|
||||||
|
* been changed, anymore.
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
* The callback, which should be removed, as an instance of the type {@link Callback}.
|
||||||
|
* The callback may not be null
|
||||||
|
*/
|
||||||
|
public final void removeCallback(@NonNull final Callback callback) {
|
||||||
|
ensureNotNull(callback, "The callback may not be null");
|
||||||
|
this.callbacks.remove(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void writeToParcel(final Parcel parcel, final int flags) {
|
||||||
|
TextUtils.writeToParcel(title, parcel, flags);
|
||||||
|
parcel.writeInt(iconId);
|
||||||
|
parcel.writeParcelable(iconBitmap, flags);
|
||||||
|
parcel.writeInt(closeable ? 1 : 0);
|
||||||
|
parcel.writeInt(closeButtonIconId);
|
||||||
|
parcel.writeParcelable(closeButtonIconBitmap, flags);
|
||||||
|
parcel.writeParcelable(backgroundColor, flags);
|
||||||
|
parcel.writeParcelable(titleTextColor, flags);
|
||||||
|
parcel.writeBundle(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/TabCloseListener.java
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified, when a tab is about to be closed by
|
||||||
|
* clicking its close button, must implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface TabCloseListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab is about to be closed by clicking its close button.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, the tab belongs to, as an instance of the class {@link
|
||||||
|
* TabSwitcher}. The tab switcher may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, which is about to be closed, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
* @return True, if the tab should be closed, false otherwise
|
||||||
|
*/
|
||||||
|
boolean onCloseTab(@NonNull TabSwitcher tabSwitcher, @NonNull Tab tab);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified, when the preview of a tab is about to
|
||||||
|
* be loaded, must implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface TabPreviewListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the preview of a tab is about to be loaded.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, which contains the tab, whose preview is about to be loaded, as an
|
||||||
|
* instance of the class {@link TabSwitcher}. The tab switcher may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose preview is about to be loaded, as an instance of the class {@link
|
||||||
|
* Tab}. The tab may not be null
|
||||||
|
* @return True, if loading the preview should be proceeded, false otherwise. When returning
|
||||||
|
* false, the method gets invoked repeatedly until true is returned.
|
||||||
|
*/
|
||||||
|
boolean onLoadTabPreview(@NonNull TabSwitcher tabSwitcher, @NonNull Tab tab);
|
||||||
|
|
||||||
|
}
|
1454
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/TabSwitcher.java
Executable file
243
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/TabSwitcherDecorator.java
Executable file
@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import de.mrapp.android.util.view.AbstractViewHolderAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for all decorators, which are responsible for inflating views, which
|
||||||
|
* should be used to visualize the tabs of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public abstract class TabSwitcherDecorator extends AbstractViewHolderAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the extra, which is used to store the state of a view hierarchy within a bundle.
|
||||||
|
*/
|
||||||
|
private static final String VIEW_HIERARCHY_STATE_EXTRA =
|
||||||
|
TabSwitcherDecorator.class.getName() + "::ViewHierarchyState";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method which is invoked, when a view, which is used to visualize a tab, should be
|
||||||
|
* inflated.
|
||||||
|
*
|
||||||
|
* @param inflater
|
||||||
|
* The inflater, which should be used to inflate the view, as an instance of the class
|
||||||
|
* {@link LayoutInflater}. The inflater may not be null
|
||||||
|
* @param parent
|
||||||
|
* The parent view of the view, which should be inflated, as an instance of the class
|
||||||
|
* {@link ViewGroup} or null, if no parent view is available
|
||||||
|
* @param viewType
|
||||||
|
* The view type of the tab, which should be visualized, as an {@link Integer} value
|
||||||
|
* @return The view, which has been inflated, as an instance of the class {@link View}. The view
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public abstract View onInflateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup parent, final int viewType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method which is invoked, when the view, which is used to visualize a tab, should be
|
||||||
|
* shown, respectively when it should be refreshed. The purpose of this method is to customize
|
||||||
|
* the appearance of the view, which is used to visualize the corresponding tab, depending on
|
||||||
|
* its state and whether the tab switcher is currently shown, or not.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, the tab switcher belongs to, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, whose tabs are visualized by the decorator, as an instance of the
|
||||||
|
* type {@link TabSwitcher}. The tab switcher may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, which is used to visualize the tab, as an instance of the class {@link
|
||||||
|
* View}. The view may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be visualized, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which should be visualized, as an {@link Integer} value
|
||||||
|
* @param viewType
|
||||||
|
* The view type of the tab, which should be visualized, as an {@link Integer} value
|
||||||
|
* @param savedInstanceState
|
||||||
|
* The bundle, which has previously been used to save the state of the view as an
|
||||||
|
* instance of the class {@link Bundle} or null, if no saved state is available
|
||||||
|
*/
|
||||||
|
public abstract void onShowTab(@NonNull final Context context,
|
||||||
|
@NonNull final TabSwitcher tabSwitcher, @NonNull final View view,
|
||||||
|
@NonNull final Tab tab, final int index, final int viewType,
|
||||||
|
@Nullable final Bundle savedInstanceState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the view, which is used to visualize a tab, is removed.
|
||||||
|
* The purpose of this method is to save the current state of the tab in a bundle.
|
||||||
|
*
|
||||||
|
* @param view
|
||||||
|
* The view, which is used to visualize the tab, as an instance of the class {@link
|
||||||
|
* View}
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose state should be saved, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, whose state should be saved, as an {@link Integer} value
|
||||||
|
* @param viewType
|
||||||
|
* The view type of the tab, whose state should be saved, as an {@link Integer} value
|
||||||
|
* @param outState
|
||||||
|
* The bundle, the state of the tab should be saved to, as an instance of the class
|
||||||
|
* {@link Bundle}. The bundle may not be null
|
||||||
|
*/
|
||||||
|
public void onSaveInstanceState(@NonNull final View view, @NonNull final Tab tab,
|
||||||
|
final int index, final int viewType,
|
||||||
|
@NonNull final Bundle outState) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view type, which corresponds to a specific tab. For each layout, which is
|
||||||
|
* inflated by the <code>onInflateView</code>-method, a distinct view type must be
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose view type should be returned, as an instance of the class {@link Tab}.
|
||||||
|
* The tab may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, whose view type should be returned, as an {@link Integer}
|
||||||
|
* value
|
||||||
|
* @return The view type, which corresponds to the given tab, as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
public int getViewType(@NonNull final Tab tab, final int index) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of view types, which are used by the decorator.
|
||||||
|
*
|
||||||
|
* @return The number of view types, which are used by the decorator, as an {@link Integer}
|
||||||
|
* value. The number of view types must correspond to the number of distinct values, which are
|
||||||
|
* returned by the <code>getViewType</code>-method
|
||||||
|
*/
|
||||||
|
public int getViewTypeCount() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked by a {@link TabSwitcher} to inflate the view, which should be
|
||||||
|
* used to visualize a specific tab.
|
||||||
|
*
|
||||||
|
* @param inflater
|
||||||
|
* The inflater, which should be used to inflate the view, as an instance of the class
|
||||||
|
* {@link LayoutInflater}. The inflater may not be null
|
||||||
|
* @param parent
|
||||||
|
* The parent view of the view, which should be inflated, as an instance of the class
|
||||||
|
* {@link ViewGroup} or null, if no parent view is available
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be visualized, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which should be visualized, as an {@link Integer} value
|
||||||
|
* @return The view, which has been inflated, as an instance of the class {@link View}. The view
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final View inflateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup parent, @NonNull final Tab tab,
|
||||||
|
final int index) {
|
||||||
|
int viewType = getViewType(tab, index);
|
||||||
|
return onInflateView(inflater, parent, viewType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked by a {@link TabSwitcher} to apply the decorator. It initializes
|
||||||
|
* the view holder pattern, which is provided by the decorator and then delegates the method
|
||||||
|
* call to the decorator's custom implementation of the method <code>onShowTab(...):void</code>.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, the tab switcher belongs to, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, whose tabs are visualized by the decorator, as an instance of the
|
||||||
|
* class {@link TabSwitcher}. The tab switcher may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, which is used to visualize the tab, as an instance of the class {@link
|
||||||
|
* View}. The view may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be visualized, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which should be visualized, as an {@link Integer} value
|
||||||
|
* @param savedInstanceState
|
||||||
|
* The bundle, which has previously been used to save the state of the view as an
|
||||||
|
* instance of the class {@link Bundle} or null, if no saved state is available
|
||||||
|
*/
|
||||||
|
public final void applyDecorator(@NonNull final Context context,
|
||||||
|
@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final View view, @NonNull final Tab tab,
|
||||||
|
final int index, @Nullable final Bundle savedInstanceState) {
|
||||||
|
setCurrentParentView(view);
|
||||||
|
int viewType = getViewType(tab, index);
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
SparseArray<Parcelable> viewStates =
|
||||||
|
savedInstanceState.getSparseParcelableArray(VIEW_HIERARCHY_STATE_EXTRA);
|
||||||
|
|
||||||
|
if (viewStates != null) {
|
||||||
|
view.restoreHierarchyState(viewStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowTab(context, tabSwitcher, view, tab, index, viewType, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked by a {@link TabSwitcher} to save the current state of a tab. It
|
||||||
|
* initializes the view holder pattern, which is provided by the decorator and then delegates
|
||||||
|
* the method call to the decorator's custom implementation of the method
|
||||||
|
* <code>onSaveInstanceState(...):void</code>.
|
||||||
|
*
|
||||||
|
* @param view
|
||||||
|
* The view, which is used to visualize the tab, as an instance of the class {@link
|
||||||
|
* View}
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose state should be saved, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, whose state should be saved, as an {@link Integer} value
|
||||||
|
* @return The bundle, which has been used to save the state, as an instance of the class {@link
|
||||||
|
* Bundle}. The bundle may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Bundle saveInstanceState(@NonNull final View view, @NonNull final Tab tab,
|
||||||
|
final int index) {
|
||||||
|
setCurrentParentView(view);
|
||||||
|
int viewType = getViewType(tab, index);
|
||||||
|
Bundle outState = new Bundle();
|
||||||
|
SparseArray<Parcelable> viewStates = new SparseArray<>();
|
||||||
|
view.saveHierarchyState(viewStates);
|
||||||
|
outState.putSparseParcelableArray(VIEW_HIERARCHY_STATE_EXTRA, viewStates);
|
||||||
|
onSaveInstanceState(view, tab, index, viewType, outState);
|
||||||
|
return outState;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
114
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/TabSwitcherListener.java
Executable file
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified about a tab switcher's events, must
|
||||||
|
* implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface TabSwitcherListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab switcher has been shown.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The observed tab switcher as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
*/
|
||||||
|
void onSwitcherShown(@NonNull TabSwitcher tabSwitcher);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab switcher has been hidden.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The observed tab switcher as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
*/
|
||||||
|
void onSwitcherHidden(@NonNull TabSwitcher tabSwitcher);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the currently selected tab has been changed.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The observed tab switcher as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
* @param selectedTabIndex
|
||||||
|
* The index of the currently selected tab as an {@link Integer} value or -1, if the tab
|
||||||
|
* switcher does not contain any tabs
|
||||||
|
* @param selectedTab
|
||||||
|
* The currently selected tab as an instance of the class {@link Tab} or null, if the
|
||||||
|
* tab switcher does not contain any tabs
|
||||||
|
*/
|
||||||
|
void onSelectionChanged(@NonNull TabSwitcher tabSwitcher, int selectedTabIndex,
|
||||||
|
@Nullable Tab selectedTab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab has been added to the tab switcher.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The observed tab switcher as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which has been added, as an {@link Integer} value
|
||||||
|
* @param tab
|
||||||
|
* The tab, which has been added, as an instance of the class {@link Tab}. The tab may
|
||||||
|
* not be null
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to add the tab, as an instance of the class {@link
|
||||||
|
* Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onTabAdded(@NonNull TabSwitcher tabSwitcher, int index, @NonNull Tab tab,
|
||||||
|
@NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab has been removed from the tab switcher.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The observed tab switcher as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which has been removed, as an {@link Integer} value
|
||||||
|
* @param tab
|
||||||
|
* The tab, which has been removed, as an instance of the class {@link Tab}. The tab may
|
||||||
|
* not be null
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to remove the tab, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onTabRemoved(@NonNull TabSwitcher tabSwitcher, int index, @NonNull Tab tab,
|
||||||
|
@NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when all tabs have been removed from the tab switcher.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The observed tab switcher as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
* @param tabs
|
||||||
|
* An array, which contains the tabs, which have been removed, as an array of the type
|
||||||
|
* {@link Tab} or an empty array, if no tabs have been removed
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to remove the tabs, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onAllTabsRemoved(@NonNull TabSwitcher tabSwitcher, @NonNull Tab[] tabs,
|
||||||
|
@NonNull Animation animation);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.drawable;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Paint.Align;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Animation;
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcherListener;
|
||||||
|
import de.mrapp.android.util.ThemeUtil;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureAtLeast;
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A drawable, which allows to display the number of tabs, which are currently contained by a {@link
|
||||||
|
* TabSwitcher}. It must be registered at a {@link TabSwitcher} instance in order to keep the
|
||||||
|
* displayed label up to date. It therefore implements the interface {@link TabSwitcherListener}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class TabSwitcherDrawable extends Drawable implements TabSwitcherListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the drawable in pixels.
|
||||||
|
*/
|
||||||
|
private final int size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default text size of the displayed label in pixels.
|
||||||
|
*/
|
||||||
|
private final int textSizeNormal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text size of the displayed label, which is used when displaying a value greater than 99,
|
||||||
|
* in pixels.
|
||||||
|
*/
|
||||||
|
private final int textSizeSmall;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drawable, which is shown as the background.
|
||||||
|
*/
|
||||||
|
private final Drawable background;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The paint, which is used to draw the drawable's label.
|
||||||
|
*/
|
||||||
|
private final Paint paint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently displayed label.
|
||||||
|
*/
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new drawable, which allows to display the number of tabs, which are currently
|
||||||
|
* contained by a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used by the drawable, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
*/
|
||||||
|
public TabSwitcherDrawable(@NonNull final Context context) {
|
||||||
|
ensureNotNull(context, "The context may not be null");
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
size = resources.getDimensionPixelSize(R.dimen.tab_switcher_drawable_size);
|
||||||
|
textSizeNormal =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.tab_switcher_drawable_font_size_normal);
|
||||||
|
textSizeSmall =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.tab_switcher_drawable_font_size_small);
|
||||||
|
background =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.tab_switcher_drawable_background)
|
||||||
|
.mutate();
|
||||||
|
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
paint.setColor(Color.WHITE);
|
||||||
|
paint.setTextAlign(Align.CENTER);
|
||||||
|
paint.setTextSize(textSizeNormal);
|
||||||
|
paint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
|
||||||
|
label = Integer.toString(0);
|
||||||
|
int tint = ThemeUtil.getColor(context, android.R.attr.textColorPrimary);
|
||||||
|
setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the drawable to display a specific value.
|
||||||
|
*
|
||||||
|
* @param count
|
||||||
|
* The value, which should be displayed, as an {@link Integer} value. The value must be
|
||||||
|
* at least 0
|
||||||
|
*/
|
||||||
|
public final void setCount(final int count) {
|
||||||
|
ensureAtLeast(count, 0, "The count must be at least 0");
|
||||||
|
label = Integer.toString(count);
|
||||||
|
|
||||||
|
if (label.length() > 2) {
|
||||||
|
label = "99+";
|
||||||
|
paint.setTextSize(textSizeSmall);
|
||||||
|
} else {
|
||||||
|
paint.setTextSize(textSizeNormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void draw(@NonNull final Canvas canvas) {
|
||||||
|
int width = canvas.getWidth();
|
||||||
|
int height = canvas.getHeight();
|
||||||
|
int intrinsicWidth = background.getIntrinsicWidth();
|
||||||
|
int intrinsicHeight = background.getIntrinsicHeight();
|
||||||
|
int left = (width / 2) - (intrinsicWidth / 2);
|
||||||
|
int top = (height / 2) - (intrinsicHeight / 2);
|
||||||
|
background.getIntrinsicWidth();
|
||||||
|
background.setBounds(left, top, left + intrinsicWidth, top + intrinsicHeight);
|
||||||
|
background.draw(canvas);
|
||||||
|
float x = width / 2f;
|
||||||
|
float y = (height / 2f) - ((paint.descent() + paint.ascent()) / 2f);
|
||||||
|
canvas.drawText(label, x, y, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getIntrinsicWidth() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getIntrinsicHeight() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setAlpha(final int alpha) {
|
||||||
|
background.setAlpha(alpha);
|
||||||
|
paint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setColorFilter(@Nullable final ColorFilter colorFilter) {
|
||||||
|
background.setColorFilter(colorFilter);
|
||||||
|
paint.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSwitcherShown(@NonNull final TabSwitcher tabSwitcher) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSwitcherHidden(@NonNull final TabSwitcher tabSwitcher) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSelectionChanged(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
final int selectedTabIndex,
|
||||||
|
@Nullable final Tab selectedTab) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabAdded(@NonNull final TabSwitcher tabSwitcher, final int index,
|
||||||
|
@NonNull final Tab tab, @NonNull final Animation animation) {
|
||||||
|
setCount(tabSwitcher.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabRemoved(@NonNull final TabSwitcher tabSwitcher, final int index,
|
||||||
|
@NonNull final Tab tab, @NonNull final Animation animation) {
|
||||||
|
setCount(tabSwitcher.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onAllTabsRemoved(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final Tab[] tab,
|
||||||
|
@NonNull final Animation animation) {
|
||||||
|
setCount(tabSwitcher.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.iterator;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureAtLeast;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for all iterators, which allow to iterate items of the type {@link
|
||||||
|
* TabItem}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractTabItemIterator implements java.util.Iterator<TabItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class of all builders, which allows to configure and create instances of the
|
||||||
|
* class {@link AbstractTabItemIterator}.
|
||||||
|
*/
|
||||||
|
public static abstract class AbstractBuilder<BuilderType extends AbstractBuilder<?, ProductType>, ProductType extends AbstractTabItemIterator> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True, if the tabs should be iterated in reverse order, false otherwise.
|
||||||
|
*/
|
||||||
|
protected boolean reverse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the first tab, which should be iterated.
|
||||||
|
*/
|
||||||
|
protected int start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reference to the builder itself. It is implicitly cast to the generic type
|
||||||
|
* BuilderType.
|
||||||
|
*
|
||||||
|
* @return The builder as an instance of the generic type BuilderType
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private BuilderType self() {
|
||||||
|
return (BuilderType) this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* AbstractTabItemIterator}.
|
||||||
|
*/
|
||||||
|
protected AbstractBuilder() {
|
||||||
|
reverse(false);
|
||||||
|
start(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the iterator, which has been configured by using the builder.
|
||||||
|
*
|
||||||
|
* @return The iterator, which has been created, as an instance of the class {@link
|
||||||
|
* TabItemIterator}. The iterator may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public abstract ProductType create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets, whether the tabs should be iterated in reverse order, or not.
|
||||||
|
*
|
||||||
|
* @param reverse
|
||||||
|
* True, if the tabs should be iterated in reverse order, false otherwise
|
||||||
|
* @return The builder, this method has been called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public BuilderType reverse(final boolean reverse) {
|
||||||
|
this.reverse = reverse;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the index of the first tab, which should be iterated.
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* The index, which should be set, as an {@link Integer} value or -1, if all tabs
|
||||||
|
* should be iterated Builder}. The builder may not be null
|
||||||
|
* @return The builder, this method has been called upon, as an instance of the generic type
|
||||||
|
* BuilderType. The builder may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public BuilderType start(final int start) {
|
||||||
|
ensureAtLeast(start, -1, "The start must be at least -1");
|
||||||
|
this.start = start;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True, if the tabs should be iterated in reverse order, false otherwise.
|
||||||
|
*/
|
||||||
|
private boolean reverse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the next tab.
|
||||||
|
*/
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current tab item.
|
||||||
|
*/
|
||||||
|
private TabItem current;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The previous tab item.
|
||||||
|
*/
|
||||||
|
private TabItem previous;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first tab item.
|
||||||
|
*/
|
||||||
|
private TabItem first;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on subclasses in order to retrieve the total number of available
|
||||||
|
* items.
|
||||||
|
*
|
||||||
|
* @return The total number of available items as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
public abstract int getCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on subclasses in order to retrieve the item, which corresponds
|
||||||
|
* to a specific index.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index of the item, which should be returned, as an {@link Integer} value
|
||||||
|
* @return The item, which corresponds to the given index, as an instance of the class {@link
|
||||||
|
* TabItem}. The tab item may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public abstract TabItem getItem(final int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the iterator.
|
||||||
|
*
|
||||||
|
* @param reverse
|
||||||
|
* True, if the tabs should be iterated in reverse order, false otherwise
|
||||||
|
* @param start
|
||||||
|
* The index of the first tab, which should be iterated, as an {@link Integer} value or
|
||||||
|
* -1, if all tabs should be iterated
|
||||||
|
*/
|
||||||
|
protected final void initialize(final boolean reverse, final int start) {
|
||||||
|
ensureAtLeast(start, -1, "The start must be at least -1");
|
||||||
|
this.reverse = reverse;
|
||||||
|
this.previous = null;
|
||||||
|
this.index = start != -1 ? start : (reverse ? getCount() - 1 : 0);
|
||||||
|
int previousIndex = reverse ? this.index + 1 : this.index - 1;
|
||||||
|
|
||||||
|
if (previousIndex >= 0 && previousIndex < getCount()) {
|
||||||
|
this.current = getItem(previousIndex);
|
||||||
|
} else {
|
||||||
|
this.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab item, which corresponds to the first tab.
|
||||||
|
*
|
||||||
|
* @return The tab item, which corresponds to the first tab, as an instance of the class {@link
|
||||||
|
* TabItem} or null, if no tabs are available
|
||||||
|
*/
|
||||||
|
public final TabItem first() {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab item, which corresponds to the previous tab.
|
||||||
|
*
|
||||||
|
* @return The tab item, which corresponds to the previous tab, as an instance of the class
|
||||||
|
* {@link TabItem} or null, if no previous tab is available
|
||||||
|
*/
|
||||||
|
public final TabItem previous() {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab item, which corresponds to the next tab.
|
||||||
|
*
|
||||||
|
* @return The tab item, which corresponds to the next tab, as an instance of the class {@link
|
||||||
|
* TabItem} or null, if no next tab is available
|
||||||
|
*/
|
||||||
|
public final TabItem peek() {
|
||||||
|
return index >= 0 && index < getCount() ? getItem(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean hasNext() {
|
||||||
|
if (reverse) {
|
||||||
|
return index >= 0;
|
||||||
|
} else {
|
||||||
|
return getCount() - index >= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final TabItem next() {
|
||||||
|
if (hasNext()) {
|
||||||
|
previous = current;
|
||||||
|
|
||||||
|
if (first == null) {
|
||||||
|
first = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = getItem(index);
|
||||||
|
index += reverse ? -1 : 1;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.iterator;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.util.view.AttachedViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An iterator, which allows to iterate the tab items, which correspond to the tabs, which are
|
||||||
|
* contained by an array.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class ArrayTabItemIterator extends AbstractTabItemIterator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder, which allows to configure an create instances of the class {@link
|
||||||
|
* ArrayTabItemIterator}.
|
||||||
|
*/
|
||||||
|
public static class Builder extends AbstractBuilder<Builder, ArrayTabItemIterator> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the
|
||||||
|
* tabs, which are iterated by the iterator, which is created by the builder.
|
||||||
|
*/
|
||||||
|
private final AttachedViewRecycler<TabItem, ?> viewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The array, which contains the tabs, which are iterated by the iterator, which is created
|
||||||
|
* by the builder.
|
||||||
|
*/
|
||||||
|
private final Tab[] array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* ArrayTabItemIterator}.
|
||||||
|
*
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize
|
||||||
|
* the tabs, which should be iterated by the iterator, as an instance of the class
|
||||||
|
* AttachedViewRecycler. The view recycler may not be null
|
||||||
|
* @param array
|
||||||
|
* The array, which contains the tabs, which should be iterated by the iterator, as
|
||||||
|
* an array of the type {@link Tab}. The array may not be null
|
||||||
|
*/
|
||||||
|
public Builder(@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler,
|
||||||
|
@NonNull final Tab[] array) {
|
||||||
|
ensureNotNull(viewRecycler, "The view recycler may not be null");
|
||||||
|
ensureNotNull(array, "The array may not be null");
|
||||||
|
this.viewRecycler = viewRecycler;
|
||||||
|
this.array = array;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ArrayTabItemIterator create() {
|
||||||
|
return new ArrayTabItemIterator(viewRecycler, array, reverse, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the
|
||||||
|
* iterated tabs.
|
||||||
|
*/
|
||||||
|
private final AttachedViewRecycler<TabItem, ?> viewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The array, which contains the tabs, which are iterated by the iterator.
|
||||||
|
*/
|
||||||
|
private final Tab[] array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new iterator, which allows to iterate the tab items, whcih correspond to the tabs,
|
||||||
|
* which are contained by an array.
|
||||||
|
*
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the
|
||||||
|
* iterated tabs, as an instance of the class AttachedViewRecycler. The view recycler
|
||||||
|
* may not be null
|
||||||
|
* @param array
|
||||||
|
* The array, which contains the tabs, which should be iterated by the iterator, as an
|
||||||
|
* array of the type {@link Tab}. The array may not be null
|
||||||
|
* @param reverse
|
||||||
|
* True, if the tabs should be iterated in reverse order, false otherwise
|
||||||
|
* @param start
|
||||||
|
* The index of the first tab, which should be iterated, as an {@link Integer} value or
|
||||||
|
* -1, if all tabs should be iterated
|
||||||
|
*/
|
||||||
|
private ArrayTabItemIterator(@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler,
|
||||||
|
@NonNull final Tab[] array, final boolean reverse,
|
||||||
|
final int start) {
|
||||||
|
ensureNotNull(viewRecycler, "The view recycler may not be null");
|
||||||
|
ensureNotNull(array, "The array may not be null");
|
||||||
|
this.viewRecycler = viewRecycler;
|
||||||
|
this.array = array;
|
||||||
|
initialize(reverse, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getCount() {
|
||||||
|
return array.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final TabItem getItem(final int index) {
|
||||||
|
return TabItem.create(viewRecycler, index, array[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.iterator;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.model.Model;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.util.view.AttachedViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An iterator, which allows to iterate the tab items, which correspond to the tabs of a {@link
|
||||||
|
* TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class TabItemIterator extends AbstractTabItemIterator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder, which allows to configure and create instances of the class {@link
|
||||||
|
* TabItemIterator}.
|
||||||
|
*/
|
||||||
|
public static class Builder extends AbstractBuilder<Builder, TabItemIterator> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model, which belongs to the tab switcher, whose tabs should be iterated by the
|
||||||
|
* iterator, which is created by the builder.
|
||||||
|
*/
|
||||||
|
private final Model model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the
|
||||||
|
* tabs, which are iterated by the iterator, which is created by the builder.
|
||||||
|
*/
|
||||||
|
private final AttachedViewRecycler<TabItem, ?> viewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new builder, which allows to configure and create instances of the class {@link
|
||||||
|
* TabItemIterator}.
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* The model, which belongs to the tab switcher, whose tabs should be iterated by
|
||||||
|
* the iterator, which is created by the builder, as an instance of the type {@link
|
||||||
|
* Model}. The model may not be null
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize
|
||||||
|
* the tabs, which are iterated by the iterator, which is created by the builder, as
|
||||||
|
* an instance of the class AttachedViewRecycler. The view recycler may not be null
|
||||||
|
*/
|
||||||
|
public Builder(@NonNull final Model model,
|
||||||
|
@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler) {
|
||||||
|
ensureNotNull(model, "The model may not be null");
|
||||||
|
ensureNotNull(viewRecycler, "The view recycler may not be null");
|
||||||
|
this.model = model;
|
||||||
|
this.viewRecycler = viewRecycler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public TabItemIterator create() {
|
||||||
|
return new TabItemIterator(model, viewRecycler, reverse, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model, which belongs to the tab switcher, whose tabs are iterated.
|
||||||
|
*/
|
||||||
|
private final Model model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which allows to inflated the views, which are used to visualize the
|
||||||
|
* iterated tabs.
|
||||||
|
*/
|
||||||
|
private final AttachedViewRecycler<TabItem, ?> viewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new iterator, which allows to iterate the tab items, which correspond to the tabs
|
||||||
|
* of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* The model, which belongs to the tab switcher, whose tabs should be iterated, as an
|
||||||
|
* instance of the type {@link Model}. The model may not be null
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the
|
||||||
|
* iterated tabs, as an instance of the class AttachedViewRecycler. The view recycler
|
||||||
|
* may not be null
|
||||||
|
* @param reverse
|
||||||
|
* True, if the tabs should be iterated in reverse order, false otherwise
|
||||||
|
* @param start
|
||||||
|
* The index of the first tab, which should be iterated, as an {@link Integer} value or
|
||||||
|
* -1, if all tabs should be iterated
|
||||||
|
*/
|
||||||
|
private TabItemIterator(@NonNull final Model model,
|
||||||
|
@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler,
|
||||||
|
final boolean reverse, final int start) {
|
||||||
|
ensureNotNull(model, "The model may not be null");
|
||||||
|
ensureNotNull(viewRecycler, "The view recycler may not be null");
|
||||||
|
this.model = model;
|
||||||
|
this.viewRecycler = viewRecycler;
|
||||||
|
initialize(reverse, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getCount() {
|
||||||
|
return model.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final TabItem getItem(final int index) {
|
||||||
|
return TabItem.create(model, viewRecycler, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,778 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.VelocityTracker;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.Arithmetics.Axis;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.util.gesture.DragHelper;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for all drag handlers, which allow to calculate the position and state of
|
||||||
|
* tabs on touch events.
|
||||||
|
*
|
||||||
|
* @param <CallbackType>
|
||||||
|
* The type of the drag handler's callback
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractDragHandler<CallbackType extends AbstractDragHandler.Callback> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all possible states of dragging gestures, which can be performed on a {@link
|
||||||
|
* TabSwitcher}.
|
||||||
|
*/
|
||||||
|
public enum DragState {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When no dragging gesture is being performed.
|
||||||
|
*/
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tabs are dragged towards the start.
|
||||||
|
*/
|
||||||
|
DRAG_TO_START,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tabs are dragged towards the end.
|
||||||
|
*/
|
||||||
|
DRAG_TO_END,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an overshoot at the start is being performed.
|
||||||
|
*/
|
||||||
|
OVERSHOOT_START,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an overshoot at the end is being performed.
|
||||||
|
*/
|
||||||
|
OVERSHOOT_END,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a tab is swiped.
|
||||||
|
*/
|
||||||
|
SWIPE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified about the events of a drag handler,
|
||||||
|
* must implement.
|
||||||
|
*/
|
||||||
|
public interface Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked in order to calculate the positions of all tabs, depending
|
||||||
|
* on the current drag distance.
|
||||||
|
*
|
||||||
|
* @param dragState
|
||||||
|
* The current drag state as a value of the enum {@link DragState}. The drag state
|
||||||
|
* must either be {@link DragState#DRAG_TO_END} or {@link DragState#DRAG_TO_START}
|
||||||
|
* @param dragDistance
|
||||||
|
* The current drag distance in pixels as a {@link Float} value
|
||||||
|
* @return A drag state, which specifies whether the tabs are overshooting, or not. If the
|
||||||
|
* tabs are overshooting, the drag state must be {@link DragState#OVERSHOOT_START} or {@link
|
||||||
|
* DragState#OVERSHOOT_END}, null otherwise
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
DragState onDrag(@NonNull DragState dragState, float dragDistance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab has been clicked.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the tab, which has been clicked, as an
|
||||||
|
* instance of the class {@link TabItem}. The tab item may not be null
|
||||||
|
*/
|
||||||
|
void onClick(@NonNull TabItem tabItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a fling has been triggered.
|
||||||
|
*
|
||||||
|
* @param distance
|
||||||
|
* The distance of the fling in pixels as a {@link Float} value
|
||||||
|
* @param duration
|
||||||
|
* The duration of the fling in milliseconds as a {@link Long} value
|
||||||
|
*/
|
||||||
|
void onFling(float distance, long duration);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a fling has been cancelled.
|
||||||
|
*/
|
||||||
|
void onCancelFling();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when an overshoot at the start should be reverted.
|
||||||
|
*/
|
||||||
|
void onRevertStartOvershoot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when an overshoot at the end should be reverted.
|
||||||
|
*/
|
||||||
|
void onRevertEndOvershoot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab is swiped.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the swiped tab, as an instance of the class
|
||||||
|
* {@link TabItem}. The tab item may not be null
|
||||||
|
* @param distance
|
||||||
|
* The distance, the tab is swiped by, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void onSwipe(@NonNull TabItem tabItem, float distance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when swiping a tab ended.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the swiped tab, as an instance of the class
|
||||||
|
* {@link TabItem}. The tab item may not be null
|
||||||
|
* @param remove
|
||||||
|
* True, if the tab should be removed, false otherwise
|
||||||
|
* @param velocity
|
||||||
|
* The velocity of the swipe gesture in pixels per second as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void onSwipeEnded(@NonNull TabItem tabItem, boolean remove, float velocity);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab switcher, whose tabs' positions and states are calculated by the drag handler.
|
||||||
|
*/
|
||||||
|
private final TabSwitcher tabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The arithmetics, which are used to calculate the positions, size and rotation of tabs.
|
||||||
|
*/
|
||||||
|
private final Arithmetics arithmetics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True, if tabs can be swiped on the orthogonal axis, false otherwise.
|
||||||
|
*/
|
||||||
|
private final boolean swipeEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag helper, which is used to recognize drag gestures on the dragging axis.
|
||||||
|
*/
|
||||||
|
private final DragHelper dragHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag helper, which is used to recognize swipe gestures on the orthogonal axis.
|
||||||
|
*/
|
||||||
|
private final DragHelper swipeDragHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum velocity, which must be reached by a drag gesture to start a fling animation.
|
||||||
|
*/
|
||||||
|
private final float minFlingVelocity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The velocity, which may be reached by a drag gesture at maximum to start a fling animation.
|
||||||
|
*/
|
||||||
|
private final float maxFlingVelocity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The velocity, which must be reached by a drag gesture in order to start a swipe animation.
|
||||||
|
*/
|
||||||
|
private final float minSwipeVelocity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The threshold, which must be reached until tabs are dragged, in pixels.
|
||||||
|
*/
|
||||||
|
private int dragThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The velocity tracker, which is used to measure the velocity of dragging gestures.
|
||||||
|
*/
|
||||||
|
private VelocityTracker velocityTracker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the pointer, which has been used to start the current drag gesture.
|
||||||
|
*/
|
||||||
|
private int pointerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently swiped tab item.
|
||||||
|
*/
|
||||||
|
private TabItem swipedTabItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the currently performed drag gesture.
|
||||||
|
*/
|
||||||
|
private DragState dragState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The distance of the current drag gesture in pixels.
|
||||||
|
*/
|
||||||
|
private float dragDistance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag distance at which the start overshoot begins.
|
||||||
|
*/
|
||||||
|
private float startOvershootThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag distance at which the end overshoot begins.
|
||||||
|
*/
|
||||||
|
private float endOvershootThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback, which is notified about the drag handler's events.
|
||||||
|
*/
|
||||||
|
private CallbackType callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the drag handler to its previous state, when a drag gesture has ended.
|
||||||
|
*
|
||||||
|
* @param dragThreshold
|
||||||
|
* The drag threshold, which should be used to recognize drag gestures, in pixels as an
|
||||||
|
* {@link Integer} value
|
||||||
|
*/
|
||||||
|
private void resetDragging(final int dragThreshold) {
|
||||||
|
if (this.velocityTracker != null) {
|
||||||
|
this.velocityTracker.recycle();
|
||||||
|
this.velocityTracker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pointerId = -1;
|
||||||
|
this.dragState = DragState.NONE;
|
||||||
|
this.swipedTabItem = null;
|
||||||
|
this.dragDistance = 0;
|
||||||
|
this.startOvershootThreshold = -Float.MAX_VALUE;
|
||||||
|
this.endOvershootThreshold = Float.MAX_VALUE;
|
||||||
|
this.dragThreshold = dragThreshold;
|
||||||
|
this.dragHelper.reset(dragThreshold);
|
||||||
|
this.swipeDragHelper.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles, when a drag gesture has been started.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* The motion event, which started the drag gesture, as an instance of the class {@link
|
||||||
|
* MotionEvent}. The motion event may not be null
|
||||||
|
*/
|
||||||
|
private void handleDown(@NonNull final MotionEvent event) {
|
||||||
|
pointerId = event.getPointerId(0);
|
||||||
|
|
||||||
|
if (velocityTracker == null) {
|
||||||
|
velocityTracker = VelocityTracker.obtain();
|
||||||
|
} else {
|
||||||
|
velocityTracker.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
velocityTracker.addMovement(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a click.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* The motion event, which triggered the click, as an instance of the class {@link
|
||||||
|
* MotionEvent}. The motion event may not be null
|
||||||
|
*/
|
||||||
|
private void handleClick(@NonNull final MotionEvent event) {
|
||||||
|
TabItem tabItem = getFocusedTab(arithmetics.getPosition(Axis.DRAGGING_AXIS, event));
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
notifyOnClick(tabItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a fling gesture.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* The motion event, which triggered the fling gesture, as an instance of the class
|
||||||
|
* {@link MotionEvent}. The motion event may not be null
|
||||||
|
* @param dragState
|
||||||
|
* The current drag state, which determines the fling direction, as a value of the enum
|
||||||
|
* {@link DragState}. The drag state may not be null
|
||||||
|
*/
|
||||||
|
private void handleFling(@NonNull final MotionEvent event, @NonNull final DragState dragState) {
|
||||||
|
int pointerId = event.getPointerId(0);
|
||||||
|
velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
|
||||||
|
float flingVelocity = Math.abs(velocityTracker.getYVelocity(pointerId));
|
||||||
|
|
||||||
|
if (flingVelocity > minFlingVelocity) {
|
||||||
|
float flingDistance = 0.25f * flingVelocity;
|
||||||
|
|
||||||
|
if (dragState == DragState.DRAG_TO_START) {
|
||||||
|
flingDistance = -1 * flingDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
long duration = Math.round(Math.abs(flingDistance) / flingVelocity * 1000);
|
||||||
|
notifyOnFling(flingDistance, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles, when the tabs are overshooting.
|
||||||
|
*/
|
||||||
|
private void handleOvershoot() {
|
||||||
|
if (!dragHelper.isReset()) {
|
||||||
|
dragHelper.reset(0);
|
||||||
|
dragDistance = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback in order to calculate the positions of all tabs, depending on the
|
||||||
|
* current drag distance.
|
||||||
|
*
|
||||||
|
* @param dragState
|
||||||
|
* The current drag state as a value of the enum {@link DragState}. The drag state must
|
||||||
|
* either be {@link DragState#DRAG_TO_END} or {@link DragState#DRAG_TO_START}
|
||||||
|
* @param dragDistance
|
||||||
|
* The current drag distance in pixels as a {@link Float} value
|
||||||
|
* @return A drag state, which specifies whether the tabs are overshooting, or not. If the tabs
|
||||||
|
* are overshooting, the drag state must be {@link DragState#OVERSHOOT_START} or {@link
|
||||||
|
* DragState#OVERSHOOT_END}, null otherwise
|
||||||
|
*/
|
||||||
|
private DragState notifyOnDrag(@NonNull final DragState dragState, final float dragDistance) {
|
||||||
|
if (callback != null) {
|
||||||
|
return callback.onDrag(dragState, dragDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that a tab has been clicked.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the tab, which has been clicked, as an instance of
|
||||||
|
* the class {@link TabItem}. The tab item may not be null
|
||||||
|
*/
|
||||||
|
private void notifyOnClick(@NonNull final TabItem tabItem) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onClick(tabItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that a fling has been triggered.
|
||||||
|
*
|
||||||
|
* @param distance
|
||||||
|
* The distance of the fling in pixels as a {@link Float} value
|
||||||
|
* @param duration
|
||||||
|
* The duration of the fling in milliseconds as a {@link Long} value
|
||||||
|
*/
|
||||||
|
private void notifyOnFling(final float distance, final long duration) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onFling(distance, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that a fling has been cancelled.
|
||||||
|
*/
|
||||||
|
private void notifyOnCancelFling() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onCancelFling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that an overshoot at the start should be reverted.
|
||||||
|
*/
|
||||||
|
private void notifyOnRevertStartOvershoot() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onRevertStartOvershoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that an overshoot at the end should be reverted.
|
||||||
|
*/
|
||||||
|
private void notifyOnRevertEndOvershoot() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onRevertEndOvershoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that a tab is swiped.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the swiped tab, as an instance of the class {@link
|
||||||
|
* TabItem}. The tab item may not be null
|
||||||
|
* @param distance
|
||||||
|
* The distance, the tab is swiped by, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private void notifyOnSwipe(@NonNull final TabItem tabItem, final float distance) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onSwipe(tabItem, distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that swiping a tab ended.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the swiped tab, as an instance of the class {@link
|
||||||
|
* TabItem}. The tab item may not be null
|
||||||
|
* @param remove
|
||||||
|
* True, if the tab should be removed, false otherwise
|
||||||
|
* @param velocity
|
||||||
|
* The velocity of the swipe gesture in pixels per second as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private void notifyOnSwipeEnded(@NonNull final TabItem tabItem, final boolean remove,
|
||||||
|
final float velocity) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onSwipeEnded(tabItem, remove, velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab switcher, whose tabs' positions and states are calculated by the drag
|
||||||
|
* handler.
|
||||||
|
*
|
||||||
|
* @return The tab switcher, whose tabs' positions and states are calculated by the drag
|
||||||
|
* handler, as an instance of the class {@link TabSwitcher}. The tab switcher may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected TabSwitcher getTabSwitcher() {
|
||||||
|
return tabSwitcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the arithmetics, which are used to calculate the positions, size and rotation of
|
||||||
|
* tabs.
|
||||||
|
*
|
||||||
|
* @return The arithmetics, which are used to calculate the positions, size and rotation of
|
||||||
|
* tabs, as an instance of the type {@link Arithmetics}. The arithmetics may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Arithmetics getArithmetics() {
|
||||||
|
return arithmetics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the callback, which should be notified about the drag handler's events.
|
||||||
|
*
|
||||||
|
* @return The callback, which should be notified about the drag handler's events, as an
|
||||||
|
* instance of the generic type CallbackType or null, if no callback should be notified
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected CallbackType getCallback() {
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new drag handler, which allows to calculate the position and state of tabs on touch
|
||||||
|
* events.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, whose tabs' positions and states should be calculated by the drag
|
||||||
|
* handler, as an instance of the class {@link TabSwitcher}. The tab switcher may not be
|
||||||
|
* null
|
||||||
|
* @param arithmetics
|
||||||
|
* The arithmetics, which should be used to calculate the position, size and rotation of
|
||||||
|
* tabs, as an instance of the type {@link Arithmetics}. The arithmetics may not be
|
||||||
|
* null
|
||||||
|
* @param swipeEnabled
|
||||||
|
* True, if tabs can be swiped on the orthogonal axis, false otherwise
|
||||||
|
*/
|
||||||
|
public AbstractDragHandler(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final Arithmetics arithmetics, final boolean swipeEnabled) {
|
||||||
|
ensureNotNull(tabSwitcher, "The tab switcher may not be null");
|
||||||
|
ensureNotNull(arithmetics, "The arithmetics may not be null");
|
||||||
|
this.tabSwitcher = tabSwitcher;
|
||||||
|
this.arithmetics = arithmetics;
|
||||||
|
this.swipeEnabled = swipeEnabled;
|
||||||
|
this.dragHelper = new DragHelper(0);
|
||||||
|
Resources resources = tabSwitcher.getResources();
|
||||||
|
this.swipeDragHelper =
|
||||||
|
new DragHelper(resources.getDimensionPixelSize(R.dimen.swipe_threshold));
|
||||||
|
this.callback = null;
|
||||||
|
ViewConfiguration configuration = ViewConfiguration.get(tabSwitcher.getContext());
|
||||||
|
this.minFlingVelocity = configuration.getScaledMinimumFlingVelocity();
|
||||||
|
this.maxFlingVelocity = configuration.getScaledMaximumFlingVelocity();
|
||||||
|
this.minSwipeVelocity = resources.getDimensionPixelSize(R.dimen.min_swipe_velocity);
|
||||||
|
resetDragging(resources.getDimensionPixelSize(R.dimen.drag_threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on implementing subclasses in order to retrieve the tab item,
|
||||||
|
* which corresponds to the tab, which is focused when clicking/dragging at a specific position.
|
||||||
|
*
|
||||||
|
* @param position
|
||||||
|
* The position on the dragging axis in pixels as a {@link Float} value
|
||||||
|
* @return The tab item, which corresponds to the focused tab, as an instance of the class
|
||||||
|
* {@link TabItem} or null, if no tab is focused
|
||||||
|
*/
|
||||||
|
protected abstract TabItem getFocusedTab(final float position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on implementing subclasses, when the tabs are overshooting at
|
||||||
|
* the start.
|
||||||
|
*
|
||||||
|
* @param dragPosition
|
||||||
|
* The position of the pointer on the dragging axis in pixels as a {@link Float} value
|
||||||
|
* @param overshootThreshold
|
||||||
|
* The position on the dragging axis, an overshoot at the start currently starts at, in
|
||||||
|
* pixels as a {@link Float} value
|
||||||
|
* @return The updated position on the dragging axis, an overshoot at the start starts at, in
|
||||||
|
* pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
protected float onOvershootStart(final float dragPosition, final float overshootThreshold) {
|
||||||
|
return overshootThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on implementing subclasses, when the tabs are overshooting at
|
||||||
|
* the end.
|
||||||
|
*
|
||||||
|
* @param dragPosition
|
||||||
|
* The position of the pointer on the dragging axis in pixels as a {@link Float} value
|
||||||
|
* @param overshootThreshold
|
||||||
|
* The position on the dragging axis, an overshoot at the end currently starts at, in
|
||||||
|
* pixels as a {@link Float} value
|
||||||
|
* @return The updated position on the dragging axis, an overshoot at the end starts at, in
|
||||||
|
* pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
protected float onOvershootEnd(final float dragPosition, final float overshootThreshold) {
|
||||||
|
return overshootThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on implementing subclasses, when an overshoot has been reverted.
|
||||||
|
*/
|
||||||
|
protected void onOvershootReverted() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which invoked on implementing subclasses, when the drag handler has been reset.
|
||||||
|
*/
|
||||||
|
protected void onReset() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the threshold of a swiped tab item, which causes the corresponding tab to be
|
||||||
|
* removed, has been reached, or not.
|
||||||
|
*
|
||||||
|
* @param swipedTabItem
|
||||||
|
* The swiped tab item as an instance of the class {@link TabItem}. The tab item may not
|
||||||
|
* be null
|
||||||
|
* @return True, if the threshold has been reached, false otherwise
|
||||||
|
*/
|
||||||
|
protected boolean isSwipeThresholdReached(@NonNull final TabItem swipedTabItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the callback, which should be notified about the drag handler's events.
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
* The callback, which should be set, as an instance of the generic type CallbackType or
|
||||||
|
* null, if no callback should be notified
|
||||||
|
*/
|
||||||
|
public final void setCallback(@Nullable final CallbackType callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a touch event.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* The event, which should be handled, as an instance of the class {@link MotionEvent}.
|
||||||
|
* The event may be not null
|
||||||
|
* @return True, if the event has been handled, false otherwise
|
||||||
|
*/
|
||||||
|
public final boolean handleTouchEvent(@NonNull final MotionEvent event) {
|
||||||
|
ensureNotNull(event, "The motion event may not be null");
|
||||||
|
|
||||||
|
if (tabSwitcher.isSwitcherShown() && !tabSwitcher.isEmpty()) {
|
||||||
|
notifyOnCancelFling();
|
||||||
|
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
handleDown(event);
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (!tabSwitcher.isAnimationRunning() && event.getPointerId(0) == pointerId) {
|
||||||
|
if (velocityTracker == null) {
|
||||||
|
velocityTracker = VelocityTracker.obtain();
|
||||||
|
}
|
||||||
|
|
||||||
|
velocityTracker.addMovement(event);
|
||||||
|
handleDrag(arithmetics.getPosition(Axis.DRAGGING_AXIS, event),
|
||||||
|
arithmetics.getPosition(Axis.ORTHOGONAL_AXIS, event));
|
||||||
|
} else {
|
||||||
|
handleRelease(null, dragThreshold);
|
||||||
|
handleDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
if (!tabSwitcher.isAnimationRunning() && event.getPointerId(0) == pointerId) {
|
||||||
|
handleRelease(event, dragThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles drag gestures.
|
||||||
|
*
|
||||||
|
* @param dragPosition
|
||||||
|
* The position of the pointer on the dragging axis in pixels as a {@link Float} value
|
||||||
|
* @param orthogonalPosition
|
||||||
|
* The position of the pointer of the orthogonal axis in pixels as a {@link Float}
|
||||||
|
* value
|
||||||
|
* @return True, if any tabs have been moved, false otherwise
|
||||||
|
*/
|
||||||
|
public final boolean handleDrag(final float dragPosition, final float orthogonalPosition) {
|
||||||
|
if (dragPosition <= startOvershootThreshold) {
|
||||||
|
handleOvershoot();
|
||||||
|
dragState = DragState.OVERSHOOT_START;
|
||||||
|
startOvershootThreshold = onOvershootStart(dragPosition, startOvershootThreshold);
|
||||||
|
} else if (dragPosition >= endOvershootThreshold) {
|
||||||
|
handleOvershoot();
|
||||||
|
dragState = DragState.OVERSHOOT_END;
|
||||||
|
endOvershootThreshold = onOvershootEnd(dragPosition, endOvershootThreshold);
|
||||||
|
} else {
|
||||||
|
onOvershootReverted();
|
||||||
|
float previousDistance = dragHelper.isReset() ? 0 : dragHelper.getDragDistance();
|
||||||
|
dragHelper.update(dragPosition);
|
||||||
|
|
||||||
|
if (swipeEnabled) {
|
||||||
|
swipeDragHelper.update(orthogonalPosition);
|
||||||
|
|
||||||
|
if (dragState == DragState.NONE && swipeDragHelper.hasThresholdBeenReached()) {
|
||||||
|
TabItem tabItem = getFocusedTab(dragHelper.getDragStartPosition());
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
dragState = DragState.SWIPE;
|
||||||
|
swipedTabItem = tabItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragState != DragState.SWIPE && dragHelper.hasThresholdBeenReached()) {
|
||||||
|
if (dragState == DragState.OVERSHOOT_START) {
|
||||||
|
dragState = DragState.DRAG_TO_END;
|
||||||
|
} else if (dragState == DragState.OVERSHOOT_END) {
|
||||||
|
dragState = DragState.DRAG_TO_START;
|
||||||
|
} else {
|
||||||
|
float dragDistance = dragHelper.getDragDistance();
|
||||||
|
|
||||||
|
if (dragDistance == 0) {
|
||||||
|
dragState = DragState.NONE;
|
||||||
|
} else {
|
||||||
|
dragState = previousDistance - dragDistance < 0 ? DragState.DRAG_TO_END :
|
||||||
|
DragState.DRAG_TO_START;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragState == DragState.SWIPE) {
|
||||||
|
notifyOnSwipe(swipedTabItem, swipeDragHelper.getDragDistance());
|
||||||
|
} else if (dragState != DragState.NONE) {
|
||||||
|
float currentDragDistance = dragHelper.getDragDistance();
|
||||||
|
float distance = currentDragDistance - dragDistance;
|
||||||
|
dragDistance = currentDragDistance;
|
||||||
|
DragState overshoot = notifyOnDrag(dragState, distance);
|
||||||
|
|
||||||
|
if (overshoot == DragState.OVERSHOOT_END && (dragState == DragState.DRAG_TO_END ||
|
||||||
|
dragState == DragState.OVERSHOOT_END)) {
|
||||||
|
endOvershootThreshold = dragPosition;
|
||||||
|
dragState = DragState.OVERSHOOT_END;
|
||||||
|
} else if (overshoot == DragState.OVERSHOOT_START &&
|
||||||
|
(dragState == DragState.DRAG_TO_START ||
|
||||||
|
dragState == DragState.OVERSHOOT_START)) {
|
||||||
|
startOvershootThreshold = dragPosition;
|
||||||
|
dragState = DragState.OVERSHOOT_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles, when a drag gesture has been ended.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* The motion event, which ended the drag gesture, as an instance of the class {@link
|
||||||
|
* MotionEvent} or null, if no fling animation should be triggered
|
||||||
|
* @param dragThreshold
|
||||||
|
* The drag threshold, which should be used to recognize drag gestures, in pixels as an
|
||||||
|
* {@link Integer} value
|
||||||
|
*/
|
||||||
|
public final void handleRelease(@Nullable final MotionEvent event, final int dragThreshold) {
|
||||||
|
if (dragState == DragState.SWIPE) {
|
||||||
|
float swipeVelocity = 0;
|
||||||
|
|
||||||
|
if (event != null && velocityTracker != null) {
|
||||||
|
int pointerId = event.getPointerId(0);
|
||||||
|
velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
|
||||||
|
swipeVelocity = Math.abs(velocityTracker.getXVelocity(pointerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean remove = swipedTabItem.getTab().isCloseable() &&
|
||||||
|
(swipeVelocity >= minSwipeVelocity || isSwipeThresholdReached(swipedTabItem));
|
||||||
|
notifyOnSwipeEnded(swipedTabItem, remove,
|
||||||
|
swipeVelocity >= minSwipeVelocity ? swipeVelocity : 0);
|
||||||
|
} else if (dragState == DragState.DRAG_TO_START || dragState == DragState.DRAG_TO_END) {
|
||||||
|
if (event != null && velocityTracker != null && dragHelper.hasThresholdBeenReached()) {
|
||||||
|
handleFling(event, dragState);
|
||||||
|
}
|
||||||
|
} else if (dragState == DragState.OVERSHOOT_END) {
|
||||||
|
notifyOnRevertEndOvershoot();
|
||||||
|
} else if (dragState == DragState.OVERSHOOT_START) {
|
||||||
|
notifyOnRevertStartOvershoot();
|
||||||
|
} else if (event != null) {
|
||||||
|
handleClick(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDragging(dragThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the drag handler to its initial state.
|
||||||
|
*
|
||||||
|
* @param dragThreshold
|
||||||
|
* The drag threshold, which should be used to recognize drag gestures, in pixels as an
|
||||||
|
* {@link Integer} value
|
||||||
|
*/
|
||||||
|
public final void reset(final int dragThreshold) {
|
||||||
|
resetDragging(dragThreshold);
|
||||||
|
onReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,616 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.Animator.AnimatorListener;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.CallSuper;
|
||||||
|
import android.support.annotation.MenuRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.util.Pair;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.DecelerateInterpolator;
|
||||||
|
import android.view.animation.Transformation;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcherDecorator;
|
||||||
|
import de.mrapp.android.tabswitcher.model.Model;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabSwitcherModel;
|
||||||
|
import de.mrapp.android.util.ViewUtil;
|
||||||
|
import de.mrapp.android.util.logging.Logger;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for all layouts, which implement the functionality of a {@link
|
||||||
|
* TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractTabSwitcherLayout
|
||||||
|
implements TabSwitcherLayout, OnGlobalLayoutListener, Model.Listener,
|
||||||
|
AbstractDragHandler.Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified about the events of a tab switcher
|
||||||
|
* layout, must implement.
|
||||||
|
*/
|
||||||
|
public interface Callback {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The method, which is invoked, when all animations have been ended.
|
||||||
|
*/
|
||||||
|
void onAnimationsEnded();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A layout listener, which unregisters itself from the observed view, when invoked. The
|
||||||
|
* listener allows to encapsulate another listener, which is notified, when the listener is
|
||||||
|
* invoked.
|
||||||
|
*/
|
||||||
|
public static class LayoutListenerWrapper implements OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The observed view.
|
||||||
|
*/
|
||||||
|
private final View view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The encapsulated listener.
|
||||||
|
*/
|
||||||
|
private final OnGlobalLayoutListener listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new layout listener, which unregisters itself from the observed view, when
|
||||||
|
* invoked.
|
||||||
|
*
|
||||||
|
* @param view
|
||||||
|
* The observed view as an instance of the class {@link View}. The view may not be
|
||||||
|
* null
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be encapsulated, as an instance of the type {@link
|
||||||
|
* OnGlobalLayoutListener} or null, if no listener should be encapsulated
|
||||||
|
*/
|
||||||
|
public LayoutListenerWrapper(@NonNull final View view,
|
||||||
|
@Nullable final OnGlobalLayoutListener listener) {
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
this.view = view;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
ViewUtil.removeOnGlobalLayoutListener(view.getViewTreeObserver(), this);
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onGlobalLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A animation listener, which increases the number of running animations, when the observed
|
||||||
|
* animation is started, and decreases the number of accordingly, when the animation is
|
||||||
|
* finished. The listener allows to encapsulate another animation listener, which is notified
|
||||||
|
* when the animation has been started, canceled or ended.
|
||||||
|
*/
|
||||||
|
protected class AnimationListenerWrapper extends AnimatorListenerAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The encapsulated listener.
|
||||||
|
*/
|
||||||
|
private final AnimatorListener listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decreases the number of running animations and executes the next pending action, if no
|
||||||
|
* running animations remain.
|
||||||
|
*/
|
||||||
|
private void endAnimation() {
|
||||||
|
if (--runningAnimations == 0) {
|
||||||
|
notifyOnAnimationsEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new animation listener, which increases the number of running animations, when
|
||||||
|
* the observed animation is started, and decreases the number of accordingly, when the
|
||||||
|
* animation is finished.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be encapsulated, as an instance of the type {@link
|
||||||
|
* AnimatorListener} or null, if no listener should be encapsulated
|
||||||
|
*/
|
||||||
|
public AnimationListenerWrapper(@Nullable final AnimatorListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(final Animator animation) {
|
||||||
|
super.onAnimationStart(animation);
|
||||||
|
runningAnimations++;
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onAnimationStart(animation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(final Animator animation) {
|
||||||
|
super.onAnimationEnd(animation);
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onAnimationEnd(animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
endAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(final Animator animation) {
|
||||||
|
super.onAnimationCancel(animation);
|
||||||
|
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onAnimationCancel(animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
endAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An animation, which allows to fling the tabs.
|
||||||
|
*/
|
||||||
|
private class FlingAnimation extends android.view.animation.Animation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The distance, the tabs should be moved.
|
||||||
|
*/
|
||||||
|
private final float distance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new fling animation.
|
||||||
|
*
|
||||||
|
* @param distance
|
||||||
|
* The distance, the tabs should be moved, in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
FlingAnimation(final float distance) {
|
||||||
|
this.distance = distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyTransformation(final float interpolatedTime, final Transformation t) {
|
||||||
|
if (flingAnimation != null) {
|
||||||
|
dragHandler.handleDrag(distance * interpolatedTime, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab switcher, the layout belongs to.
|
||||||
|
*/
|
||||||
|
private final TabSwitcher tabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model of the tab switcher, the layout belongs to.
|
||||||
|
*/
|
||||||
|
private final TabSwitcherModel model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The arithmetics, which are used by the layout.
|
||||||
|
*/
|
||||||
|
private final Arithmetics arithmetics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The threshold, which must be reached until tabs are dragged, in pixels.
|
||||||
|
*/
|
||||||
|
private final int dragThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logger, which is used for logging.
|
||||||
|
*/
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback, which is notified about the layout's events.
|
||||||
|
*/
|
||||||
|
private Callback callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of animations, which are currently running.
|
||||||
|
*/
|
||||||
|
private int runningAnimations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The animation, which is used to fling the tabs.
|
||||||
|
*/
|
||||||
|
private android.view.animation.Animation flingAnimation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag handler, which is used by the layout.
|
||||||
|
*/
|
||||||
|
private AbstractDragHandler<?> dragHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the visibility of the toolbars, which are shown, when the tab switcher is shown.
|
||||||
|
*/
|
||||||
|
private void adaptToolbarVisibility() {
|
||||||
|
Toolbar[] toolbars = getToolbars();
|
||||||
|
|
||||||
|
if (toolbars != null) {
|
||||||
|
for (Toolbar toolbar : toolbars) {
|
||||||
|
toolbar.setVisibility(
|
||||||
|
getModel().areToolbarsShown() ? View.VISIBLE : View.INVISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the title of the toolbar, which is shown, when the tab switcher is shown.
|
||||||
|
*/
|
||||||
|
private void adaptToolbarTitle() {
|
||||||
|
Toolbar[] toolbars = getToolbars();
|
||||||
|
|
||||||
|
if (toolbars != null) {
|
||||||
|
toolbars[0].setTitle(getModel().getToolbarTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the navigation icon of the toolbar, which is shown, when the tab switcher is shown.
|
||||||
|
*/
|
||||||
|
private void adaptToolbarNavigationIcon() {
|
||||||
|
Toolbar[] toolbars = getToolbars();
|
||||||
|
|
||||||
|
if (toolbars != null) {
|
||||||
|
Toolbar toolbar = toolbars[0];
|
||||||
|
toolbar.setNavigationIcon(getModel().getToolbarNavigationIcon());
|
||||||
|
toolbar.setNavigationOnClickListener(getModel().getToolbarNavigationIconListener());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inflates the menu of the toolbar, which is shown, when the tab switcher is shown.
|
||||||
|
*/
|
||||||
|
private void inflateToolbarMenu() {
|
||||||
|
Toolbar[] toolbars = getToolbars();
|
||||||
|
int menuId = getModel().getToolbarMenuId();
|
||||||
|
|
||||||
|
if (toolbars != null && menuId != -1) {
|
||||||
|
Toolbar toolbar = toolbars.length > 1 ? toolbars[1] : toolbars[0];
|
||||||
|
toolbar.inflateMenu(menuId);
|
||||||
|
toolbar.setOnMenuItemClickListener(getModel().getToolbarMenuItemListener());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns an animation listener, which allows to handle, when a fling animation
|
||||||
|
* ended.
|
||||||
|
*
|
||||||
|
* @return The listener, which has been created, as an instance of the class {@link
|
||||||
|
* Animation.AnimationListener}. The listener may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private Animation.AnimationListener createFlingAnimationListener() {
|
||||||
|
return new Animation.AnimationListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(final android.view.animation.Animation animation) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(final android.view.animation.Animation animation) {
|
||||||
|
dragHandler.handleRelease(null, dragThreshold);
|
||||||
|
flingAnimation = null;
|
||||||
|
notifyOnAnimationsEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(final android.view.animation.Animation animation) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that all animations have been ended.
|
||||||
|
*/
|
||||||
|
private void notifyOnAnimationsEnded() {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onAnimationsEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab switcher, the layout belongs to.
|
||||||
|
*
|
||||||
|
* @return The tab switcher, the layout belongs to, as an instance of the class {@link
|
||||||
|
* TabSwitcher}. The tab switcher may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected final TabSwitcher getTabSwitcher() {
|
||||||
|
return tabSwitcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the model of the tab switcher, the layout belongs to.
|
||||||
|
*
|
||||||
|
* @return The model of the tab switcher, the layout belongs to, as an instance of the class
|
||||||
|
* {@link TabSwitcherModel}. The model may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected final TabSwitcherModel getModel() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the arithmetics, which are used by the layout.
|
||||||
|
*
|
||||||
|
* @return The arithmetics, which are used by the layout, as an instance of the type {@link
|
||||||
|
* Arithmetics}. The arithmetics may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected final Arithmetics getArithmetics() {
|
||||||
|
return arithmetics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the threshold, which must be reached until tabs are dragged.
|
||||||
|
*
|
||||||
|
* @return The threshold, which must be reached until tabs are dragged, in pixels as an {@link
|
||||||
|
* Integer} value
|
||||||
|
*/
|
||||||
|
protected final int getDragThreshold() {
|
||||||
|
return dragThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the logger, which is used for logging.
|
||||||
|
*
|
||||||
|
* @return The logger, which is used for logging, as an instance of the class Logger. The logger
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected final Logger getLogger() {
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the context, which is used by the layout.
|
||||||
|
*
|
||||||
|
* @return The context, which is used by the layout, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected final Context getContext() {
|
||||||
|
return tabSwitcher.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new layout, which implements the functionality of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, the layout belongs to, as an instance of the class {@link
|
||||||
|
* TabSwitcher}. The tab switcher may not be null
|
||||||
|
* @param model
|
||||||
|
* The model of the tab switcher, the layout belongs to, as an instance of the class
|
||||||
|
* {@link TabSwitcherModel}. The model may not be null
|
||||||
|
* @param arithmetics
|
||||||
|
* The arithmetics, which should be used by the layout, as an instance of the type
|
||||||
|
* {@link Arithmetics}. The arithmetics may not be null
|
||||||
|
*/
|
||||||
|
public AbstractTabSwitcherLayout(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final TabSwitcherModel model,
|
||||||
|
@NonNull final Arithmetics arithmetics) {
|
||||||
|
ensureNotNull(tabSwitcher, "The tab switcher may not be null");
|
||||||
|
ensureNotNull(model, "The model may not be null");
|
||||||
|
ensureNotNull(arithmetics, "The arithmetics may not be null");
|
||||||
|
this.tabSwitcher = tabSwitcher;
|
||||||
|
this.model = model;
|
||||||
|
this.arithmetics = arithmetics;
|
||||||
|
this.dragThreshold =
|
||||||
|
getTabSwitcher().getResources().getDimensionPixelSize(R.dimen.drag_threshold);
|
||||||
|
this.logger = new Logger(model.getLogLevel());
|
||||||
|
this.callback = null;
|
||||||
|
this.runningAnimations = 0;
|
||||||
|
this.flingAnimation = null;
|
||||||
|
this.dragHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on implementing subclasses in order to inflate the layout.
|
||||||
|
*
|
||||||
|
* @param tabsOnly
|
||||||
|
* True, if only the tabs should be inflated, false otherwise
|
||||||
|
* @return The drag handler, which is used by the layout, as an instance of the class {@link
|
||||||
|
* AbstractDragHandler} or null, if no drag handler is used
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract AbstractDragHandler<?> onInflateLayout(final boolean tabsOnly);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked on implementing subclasses in order to detach the layout.
|
||||||
|
*
|
||||||
|
* @param tabsOnly
|
||||||
|
* True, if only the tabs should be detached, false otherwise
|
||||||
|
* @return A pair, which contains the index of the first visible tab, as well as its current
|
||||||
|
* position, as an instance of the class Pair or null, if the tab switcher is not shown
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected abstract Pair<Integer, Float> onDetachLayout(final boolean tabsOnly);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a touch event.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* The touch event as an instance of the class {@link MotionEvent}. The touch event may
|
||||||
|
* not be null
|
||||||
|
* @return True, if the event has been handled, false otherwise
|
||||||
|
*/
|
||||||
|
public abstract boolean handleTouchEvent(@NonNull final MotionEvent event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inflates the layout.
|
||||||
|
*
|
||||||
|
* @param tabsOnly
|
||||||
|
* True, if only the tabs should be inflated, false otherwise
|
||||||
|
*/
|
||||||
|
public final void inflateLayout(final boolean tabsOnly) {
|
||||||
|
dragHandler = onInflateLayout(tabsOnly);
|
||||||
|
|
||||||
|
if (!tabsOnly) {
|
||||||
|
adaptToolbarVisibility();
|
||||||
|
adaptToolbarTitle();
|
||||||
|
adaptToolbarNavigationIcon();
|
||||||
|
inflateToolbarMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches the layout.
|
||||||
|
*
|
||||||
|
* @param tabsOnly
|
||||||
|
* True, if only the tabs should be detached, false otherwise
|
||||||
|
* @return A pair, which contains the index of the first visible tab, as well as its current
|
||||||
|
* position, as an instance of the class Pair or null, if the tab switcher is not shown
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final Pair<Integer, Float> detachLayout(final boolean tabsOnly) {
|
||||||
|
return onDetachLayout(tabsOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the callback, which should be notified about the layout's events.
|
||||||
|
*
|
||||||
|
* @param callback
|
||||||
|
* The callback, which should be set, as an instance of the type {@link Callback} or
|
||||||
|
* null, if no callback should be notified
|
||||||
|
*/
|
||||||
|
public final void setCallback(@Nullable final Callback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean isAnimationRunning() {
|
||||||
|
return runningAnimations > 0 || flingAnimation != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public final Menu getToolbarMenu() {
|
||||||
|
Toolbar[] toolbars = getToolbars();
|
||||||
|
|
||||||
|
if (toolbars != null) {
|
||||||
|
Toolbar toolbar = toolbars.length > 1 ? toolbars[1] : toolbars[0];
|
||||||
|
return toolbar.getMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
@Override
|
||||||
|
public void onDecoratorChanged(@NonNull final TabSwitcherDecorator decorator) {
|
||||||
|
detachLayout(true);
|
||||||
|
onGlobalLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarVisibilityChanged(final boolean visible) {
|
||||||
|
adaptToolbarVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarTitleChanged(@Nullable final CharSequence title) {
|
||||||
|
adaptToolbarTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarNavigationIconChanged(@Nullable final Drawable icon,
|
||||||
|
@Nullable final OnClickListener listener) {
|
||||||
|
adaptToolbarNavigationIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarMenuInflated(@MenuRes final int resourceId,
|
||||||
|
@Nullable final OnMenuItemClickListener listener) {
|
||||||
|
inflateToolbarMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onFling(final float distance, final long duration) {
|
||||||
|
if (dragHandler != null) {
|
||||||
|
flingAnimation = new FlingAnimation(distance);
|
||||||
|
flingAnimation.setFillAfter(true);
|
||||||
|
flingAnimation.setAnimationListener(createFlingAnimationListener());
|
||||||
|
flingAnimation.setDuration(duration);
|
||||||
|
flingAnimation.setInterpolator(new DecelerateInterpolator());
|
||||||
|
getTabSwitcher().startAnimation(flingAnimation);
|
||||||
|
logger.logVerbose(getClass(),
|
||||||
|
"Started fling animation using a distance of " + distance +
|
||||||
|
" pixels and a duration of " + duration + " milliseconds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onCancelFling() {
|
||||||
|
if (flingAnimation != null) {
|
||||||
|
flingAnimation.cancel();
|
||||||
|
flingAnimation = null;
|
||||||
|
dragHandler.handleRelease(null, dragThreshold);
|
||||||
|
logger.logVerbose(getClass(), "Canceled fling animation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevertStartOvershoot() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevertEndOvershoot() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwipe(@NonNull final TabItem tabItem, final float distance) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwipeEnded(@NonNull final TabItem tabItem, final boolean remove,
|
||||||
|
final float velocity) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout;
|
||||||
|
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract base class for all view holders, which allow to store references to the views, a tab
|
||||||
|
* of a {@link TabSwitcher} consists of.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractTabViewHolder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text view, which is used to display the title of a tab.
|
||||||
|
*/
|
||||||
|
public TextView titleTextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The close button of a tab.
|
||||||
|
*/
|
||||||
|
public ImageButton closeButton;
|
||||||
|
|
||||||
|
}
|
274
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/layout/Arithmetics.java
Executable file
@ -0,0 +1,274 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.mrapp.android.tabswitcher.layout;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewPropertyAnimator;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.AbstractDragHandler.DragState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which provides methods, which allow to calculate the position,
|
||||||
|
* size and rotation of a {@link TabSwitcher}'s children, must implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface Arithmetics {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all axes on which the tabs of a {@link TabSwitcher} can be moved.
|
||||||
|
*/
|
||||||
|
enum Axis {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The axis on which a tab is moved when dragging it.
|
||||||
|
*/
|
||||||
|
DRAGGING_AXIS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The axis on which a tab is moved, when it is added to or removed from the switcher.
|
||||||
|
*/
|
||||||
|
ORTHOGONAL_AXIS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The horizontal axis.
|
||||||
|
*/
|
||||||
|
X_AXIS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vertical axis.
|
||||||
|
*/
|
||||||
|
Y_AXIS
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the position of a motion event on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param event
|
||||||
|
* The motion event, whose position should be returned, as an instance of the class
|
||||||
|
* {@link MotionEvent}. The motion event may not be null
|
||||||
|
* @return The position of the given motion event on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getPosition(@NonNull Axis axis, @NonNull MotionEvent event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the position of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose position should be returned, as an instance of the class {@link
|
||||||
|
* View}. The view may not be null
|
||||||
|
* @return The position of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getPosition(@NonNull Axis axis, @NonNull View view);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose position should be set, as an instance of the class {@link View}. The
|
||||||
|
* view may not be null
|
||||||
|
* @param position
|
||||||
|
* The position, which should be set, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void setPosition(@NonNull Axis axis, @NonNull View view, float position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the position of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param animator
|
||||||
|
* The animator, which should be used to animate the position, as an instance of the
|
||||||
|
* class {@link ViewPropertyAnimator}. The animator may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose position should be animated, as an instance of the class {@link
|
||||||
|
* View}. The view may not be null
|
||||||
|
* @param position
|
||||||
|
* The position, which should be set by the animation, as a {@link Float} value
|
||||||
|
* @param includePadding
|
||||||
|
* True, if the view's padding should be taken into account, false otherwise
|
||||||
|
*/
|
||||||
|
void animatePosition(@NonNull Axis axis, @NonNull ViewPropertyAnimator animator,
|
||||||
|
@NonNull View view, float position, boolean includePadding);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the padding of a view on a specific axis and using a specific gravity.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param gravity
|
||||||
|
* The gravity as an {@link Integer} value. The gravity must be
|
||||||
|
* <code>Gravity.START</code> or <code>Gravity.END</code>
|
||||||
|
* @param view
|
||||||
|
* The view, whose padding should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @return The padding of the given view on the given axis and using the given gravity as an
|
||||||
|
* {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPadding(@NonNull Axis axis, int gravity, @NonNull View view);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scale of a view, depending on its margin.
|
||||||
|
*
|
||||||
|
* @param view
|
||||||
|
* The view, whose scale should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @param includePadding
|
||||||
|
* True, if the view's padding should be taken into account as well, false otherwise
|
||||||
|
* @return The scale of the given view as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getScale(@NonNull final View view, final boolean includePadding);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the scale of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose scale should be set, as an instance of the class {@link View}. The
|
||||||
|
* view may not be null
|
||||||
|
* @param scale
|
||||||
|
* The scale, which should be set, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void setScale(@NonNull Axis axis, @NonNull View view, float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the scale of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param animator
|
||||||
|
* The animator, which should be used to animate the scale, as an instance of the class
|
||||||
|
* {@link ViewPropertyAnimator}. The animator may not be null
|
||||||
|
* @param scale
|
||||||
|
* The scale, which should be set by the animation, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void animateScale(@NonNull Axis axis, @NonNull ViewPropertyAnimator animator, float scale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose size should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @return The size of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getSize(@NonNull Axis axis, @NonNull View view);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the container, which contains the tab switcher's tabs, on a specific
|
||||||
|
* axis. By default, the padding and the size of the toolbars are included.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @return The size of the container, which contains the tab switcher's tabs, on the given axis
|
||||||
|
* as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getTabContainerSize(@NonNull Axis axis);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the container, which contains the tab switcher's tabs, on a specific
|
||||||
|
* axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param includePadding
|
||||||
|
* True, if the padding and the size of the toolbars should be included, false
|
||||||
|
* otherwise
|
||||||
|
* @return The size of the container, which contains the tab switcher's tabs, on the given axis
|
||||||
|
* as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getTabContainerSize(@NonNull Axis axis, boolean includePadding);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the pivot of a view on a specific axis, depending on the current drag state.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose pivot should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @param dragState
|
||||||
|
* The current drag state as a value of the enum {@link DragState}. The drag state may
|
||||||
|
* not be null
|
||||||
|
* @return The pivot of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getPivot(@NonNull Axis axis, @NonNull View view, @NonNull DragState dragState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the pivot of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose pivot should be set, as an instance of the class {@link View}. The
|
||||||
|
* view may not be null
|
||||||
|
* @param pivot
|
||||||
|
* The pivot, which should be set, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void setPivot(@NonNull Axis axis, @NonNull View view, float pivot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the rotation of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose rotation should be returned, as an instance of the class {@link
|
||||||
|
* View}. The view may not be null
|
||||||
|
* @return The rotation of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
float getRotation(@NonNull Axis axis, @NonNull View view);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the rotation of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose rotation should be set, as an instance of the class {@link View}. The
|
||||||
|
* view may not be null
|
||||||
|
* @param angle
|
||||||
|
* The rotation, which should be set, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void setRotation(@NonNull Axis axis, @NonNull View view, float angle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the rotation of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param animator
|
||||||
|
* The animator, should be used to animate the rotation, as an instance of the class
|
||||||
|
* {@link ViewPropertyAnimator}. The animator may not be null
|
||||||
|
* @param angle
|
||||||
|
* The rotation, which should be set by the animation, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void animateRotation(@NonNull Axis axis, @NonNull ViewPropertyAnimator animator, float angle);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcherDecorator;
|
||||||
|
import de.mrapp.android.tabswitcher.model.Restorable;
|
||||||
|
import de.mrapp.android.util.view.AbstractViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view recycler adapter, which allows to inflate the views, which are used to visualize the child
|
||||||
|
* views of the tabs of a {@link TabSwitcher}, by encapsulating a {@link TabSwitcherDecorator}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class ChildRecyclerAdapter extends AbstractViewRecycler.Adapter<Tab, Void>
|
||||||
|
implements Restorable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the extra, which is used to store the saved instance states of previously removed
|
||||||
|
* child views within a bundle.
|
||||||
|
*/
|
||||||
|
private static final String SAVED_INSTANCE_STATES_EXTRA =
|
||||||
|
ChildRecyclerAdapter.class.getName() + "::SavedInstanceStates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab switcher, which contains the tabs, the child views, which are inflated by the
|
||||||
|
* adapter, correspond to.
|
||||||
|
*/
|
||||||
|
private final TabSwitcher tabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decorator, which is used to inflate the child views.
|
||||||
|
*/
|
||||||
|
private final TabSwitcherDecorator decorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sparse array, which manages the saved instance states of previously removed child views.
|
||||||
|
*/
|
||||||
|
private SparseArray<Bundle> savedInstanceStates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new view recycler adapter, which allows to inflate the views, which are used to
|
||||||
|
* visualize the child views of the tabs of a {@link TabSwitcher}, by encapsulating a {@link
|
||||||
|
* TabSwitcherDecorator}.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, which contains the tabs, the child views, which are inflated by the
|
||||||
|
* adapter, correspond to, as an instance of the class {@link TabSwitcher}. The tab
|
||||||
|
* switcher may not be null
|
||||||
|
* @param decorator
|
||||||
|
* The decorator, which should be used to inflate the child views, as an instance of the
|
||||||
|
* class {@link TabSwitcherDecorator}. The decorator may not be null
|
||||||
|
*/
|
||||||
|
public ChildRecyclerAdapter(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final TabSwitcherDecorator decorator) {
|
||||||
|
ensureNotNull(tabSwitcher, "The tab switcher may not be null");
|
||||||
|
ensureNotNull(decorator, "The decorator may not be null");
|
||||||
|
this.tabSwitcher = tabSwitcher;
|
||||||
|
this.decorator = decorator;
|
||||||
|
this.savedInstanceStates = new SparseArray<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final View onInflateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup parent, @NonNull final Tab item,
|
||||||
|
final int viewType, @NonNull final Void... params) {
|
||||||
|
int index = tabSwitcher.indexOf(item);
|
||||||
|
return decorator.inflateView(inflater, parent, item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onShowView(@NonNull final Context context, @NonNull final View view,
|
||||||
|
@NonNull final Tab item, final boolean inflated,
|
||||||
|
@NonNull final Void... params) {
|
||||||
|
int index = tabSwitcher.indexOf(item);
|
||||||
|
Bundle savedInstanceState = savedInstanceStates.get(item.hashCode());
|
||||||
|
decorator.applyDecorator(context, tabSwitcher, view, item, index, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onRemoveView(@NonNull final View view, @NonNull final Tab item) {
|
||||||
|
int index = tabSwitcher.indexOf(item);
|
||||||
|
Bundle outState = decorator.saveInstanceState(view, item, index);
|
||||||
|
savedInstanceStates.put(item.hashCode(), outState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getViewTypeCount() {
|
||||||
|
return decorator.getViewTypeCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getViewType(@NonNull final Tab item) {
|
||||||
|
int index = tabSwitcher.indexOf(item);
|
||||||
|
return decorator.getViewType(item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void saveInstanceState(@NonNull final Bundle outState) {
|
||||||
|
outState.putSparseParcelableArray(SAVED_INSTANCE_STATES_EXTRA, savedInstanceStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void restoreInstanceState(@Nullable final Bundle savedInstanceState) {
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
savedInstanceStates =
|
||||||
|
savedInstanceState.getSparseParcelableArray(SAVED_INSTANCE_STATES_EXTRA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a layout, which implements the functionality of a {@link TabSwitcher},
|
||||||
|
* must implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface TabSwitcherLayout {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether an animation is currently running, or not.
|
||||||
|
*
|
||||||
|
* @return True, if an animation is currently running, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isAnimationRunning();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view group, which contains the tab switcher's tabs.
|
||||||
|
*
|
||||||
|
* @return The view group, which contains the tab switcher's tabs, as an instance of the class
|
||||||
|
* {@link ViewGroup} or null, if the view has not been laid out yet
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
ViewGroup getTabContainer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the toolbars, which are shown, when the tab switcher is shown. When using the
|
||||||
|
* smartphone layout, only one toolbar is shown. When using the tablet layout, a primary and
|
||||||
|
* secondary toolbar is shown. In such case, the first index of the returned array corresponds
|
||||||
|
* to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @return An array, which contains the toolbars, which are shown, when the tab switcher is
|
||||||
|
* shown, as an array of the type Toolbar or null, if the view has not been laid out yet
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
Toolbar[] getToolbars();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the menu of the toolbar, which is shown, when the tab switcher is shown. When using
|
||||||
|
* the tablet layout, the menu corresponds to the secondary toolbar.
|
||||||
|
*
|
||||||
|
* @return The menu of the toolbar as an instance of the type {@link Menu} or null, if the view
|
||||||
|
* has not been laid out yet
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
Menu getToolbarMenu();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,439 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout.phone;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewPropertyAnimator;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Layout;
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.AbstractDragHandler.DragState;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.Arithmetics;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
import static de.mrapp.android.util.Condition.ensureTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods, which allow to calculate the position, size and rotation of a {@link
|
||||||
|
* TabSwitcher}'s children, when using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class PhoneArithmetics implements Arithmetics {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab switcher, the arithmetics are calculated for.
|
||||||
|
*/
|
||||||
|
private final TabSwitcher tabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The height of a tab's title container in pixels.
|
||||||
|
*/
|
||||||
|
private final int tabTitleContainerHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The inset of tabs in pixels.
|
||||||
|
*/
|
||||||
|
private final int tabInset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of tabs, which are contained by a stack.
|
||||||
|
*/
|
||||||
|
private final int stackedTabCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The space between tabs, which are part of a stack, in pixels.
|
||||||
|
*/
|
||||||
|
private final float stackedTabSpacing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pivot when overshooting at the end.
|
||||||
|
*/
|
||||||
|
private final float endOvershootPivot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies a specific axis depending on the orientation of the tab switcher.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The original axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @return The orientation invariant axis as a value of the enum {@link Axis}. The orientation
|
||||||
|
* invariant axis may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private Axis getOrientationInvariantAxis(@NonNull final Axis axis) {
|
||||||
|
if (axis == Axis.Y_AXIS) {
|
||||||
|
return Axis.DRAGGING_AXIS;
|
||||||
|
} else if (axis == Axis.X_AXIS) {
|
||||||
|
return Axis.ORTHOGONAL_AXIS;
|
||||||
|
} else if (tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE) {
|
||||||
|
return axis == Axis.DRAGGING_AXIS ? Axis.ORTHOGONAL_AXIS : Axis.DRAGGING_AXIS;
|
||||||
|
} else {
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default pivot of a view on a specific axis.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose pivot should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @return The pivot of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private float getDefaultPivot(@NonNull final Axis axis, @NonNull final View view) {
|
||||||
|
if (axis == Axis.DRAGGING_AXIS || axis == Axis.Y_AXIS) {
|
||||||
|
return tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ? getSize(axis, view) / 2f : 0;
|
||||||
|
} else {
|
||||||
|
return tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ? 0 : getSize(axis, view) / 2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the pivot of a view on a specific axis, when it is swiped.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose pivot should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @return The pivot of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private float getPivotWhenSwiping(@NonNull final Axis axis, @NonNull final View view) {
|
||||||
|
if (axis == Axis.DRAGGING_AXIS || axis == Axis.Y_AXIS) {
|
||||||
|
return endOvershootPivot;
|
||||||
|
} else {
|
||||||
|
return getDefaultPivot(axis, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the pivot of a view on a specific axis, when overshooting at the start.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose pivot should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @return The pivot of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private float getPivotWhenOvershootingAtStart(@NonNull final Axis axis,
|
||||||
|
@NonNull final View view) {
|
||||||
|
return getSize(axis, view) / 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the pivot of a view on a specific axis, when overshooting at the end.
|
||||||
|
*
|
||||||
|
* @param axis
|
||||||
|
* The axis as a value of the enum {@link Axis}. The axis may not be null
|
||||||
|
* @param view
|
||||||
|
* The view, whose pivot should be returned, as an instance of the class {@link View}.
|
||||||
|
* The view may not be null
|
||||||
|
* @return The pivot of the given view on the given axis as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private float getPivotWhenOvershootingAtEnd(@NonNull final Axis axis,
|
||||||
|
@NonNull final View view) {
|
||||||
|
if (axis == Axis.DRAGGING_AXIS || axis == Axis.Y_AXIS) {
|
||||||
|
return tabSwitcher.getCount() > 1 ? endOvershootPivot : getSize(axis, view) / 2f;
|
||||||
|
} else {
|
||||||
|
return getSize(axis, view) / 2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new class, which provides methods, which allow to calculate the position, size and
|
||||||
|
* rotation of a {@link TabSwitcher}'s children.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, the arithmetics should be calculated for, as an instance of the
|
||||||
|
* class {@link TabSwitcher}. The tab switcher may not be null
|
||||||
|
*/
|
||||||
|
public PhoneArithmetics(@NonNull final TabSwitcher tabSwitcher) {
|
||||||
|
ensureNotNull(tabSwitcher, "The tab switcher may not be null");
|
||||||
|
this.tabSwitcher = tabSwitcher;
|
||||||
|
Resources resources = tabSwitcher.getResources();
|
||||||
|
this.tabTitleContainerHeight =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.tab_title_container_height);
|
||||||
|
this.tabInset = resources.getDimensionPixelSize(R.dimen.tab_inset);
|
||||||
|
this.stackedTabCount = resources.getInteger(R.integer.stacked_tab_count);
|
||||||
|
this.stackedTabSpacing = resources.getDimensionPixelSize(R.dimen.stacked_tab_spacing);
|
||||||
|
this.endOvershootPivot = resources.getDimensionPixelSize(R.dimen.end_overshoot_pivot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getPosition(@NonNull final Axis axis, @NonNull final MotionEvent event) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(event, "The motion event may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
return event.getY();
|
||||||
|
} else {
|
||||||
|
return event.getX();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getPosition(@NonNull final Axis axis, @NonNull final View view) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
Toolbar[] toolbars = tabSwitcher.getToolbars();
|
||||||
|
return view.getY() - (tabSwitcher.areToolbarsShown() && tabSwitcher.isSwitcherShown() &&
|
||||||
|
toolbars != null ? toolbars[0].getHeight() - tabInset : 0) -
|
||||||
|
getPadding(axis, Gravity.START, tabSwitcher);
|
||||||
|
} else {
|
||||||
|
FrameLayout.LayoutParams layoutParams =
|
||||||
|
(FrameLayout.LayoutParams) view.getLayoutParams();
|
||||||
|
return view.getX() - layoutParams.leftMargin - tabSwitcher.getPaddingLeft() / 2f +
|
||||||
|
tabSwitcher.getPaddingRight() / 2f +
|
||||||
|
(tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE &&
|
||||||
|
tabSwitcher.isSwitcherShown() ?
|
||||||
|
stackedTabCount * stackedTabSpacing / 2f : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setPosition(@NonNull final Axis axis, @NonNull final View view,
|
||||||
|
final float position) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
Toolbar[] toolbars = tabSwitcher.getToolbars();
|
||||||
|
view.setY((tabSwitcher.areToolbarsShown() && tabSwitcher.isSwitcherShown() &&
|
||||||
|
toolbars != null ? toolbars[0].getHeight() - tabInset : 0) +
|
||||||
|
getPadding(axis, Gravity.START, tabSwitcher) + position);
|
||||||
|
} else {
|
||||||
|
FrameLayout.LayoutParams layoutParams =
|
||||||
|
(FrameLayout.LayoutParams) view.getLayoutParams();
|
||||||
|
view.setX(position + layoutParams.leftMargin + tabSwitcher.getPaddingLeft() / 2f -
|
||||||
|
tabSwitcher.getPaddingRight() / 2f -
|
||||||
|
(tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE &&
|
||||||
|
tabSwitcher.isSwitcherShown() ?
|
||||||
|
stackedTabCount * stackedTabSpacing / 2f : 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void animatePosition(@NonNull final Axis axis,
|
||||||
|
@NonNull final ViewPropertyAnimator animator,
|
||||||
|
@NonNull final View view, final float position,
|
||||||
|
final boolean includePadding) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(animator, "The animator may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
Toolbar[] toolbars = tabSwitcher.getToolbars();
|
||||||
|
animator.y((tabSwitcher.areToolbarsShown() && tabSwitcher.isSwitcherShown() &&
|
||||||
|
toolbars != null ? toolbars[0].getHeight() - tabInset : 0) +
|
||||||
|
(includePadding ? getPadding(axis, Gravity.START, tabSwitcher) : 0) + position);
|
||||||
|
} else {
|
||||||
|
FrameLayout.LayoutParams layoutParams =
|
||||||
|
(FrameLayout.LayoutParams) view.getLayoutParams();
|
||||||
|
animator.x(position + layoutParams.leftMargin + (includePadding ?
|
||||||
|
tabSwitcher.getPaddingLeft() / 2f - tabSwitcher.getPaddingRight() / 2f : 0) -
|
||||||
|
(tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE &&
|
||||||
|
tabSwitcher.isSwitcherShown() ?
|
||||||
|
stackedTabCount * stackedTabSpacing / 2f : 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getPadding(@NonNull final Axis axis, final int gravity,
|
||||||
|
@NonNull final View view) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureTrue(gravity == Gravity.START || gravity == Gravity.END, "Invalid gravity");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
return gravity == Gravity.START ? view.getPaddingTop() : view.getPaddingBottom();
|
||||||
|
} else {
|
||||||
|
return gravity == Gravity.START ? view.getPaddingLeft() : view.getPaddingRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getScale(@NonNull final View view, final boolean includePadding) {
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view.getLayoutParams();
|
||||||
|
float width = view.getWidth();
|
||||||
|
float targetWidth = width + layoutParams.leftMargin + layoutParams.rightMargin -
|
||||||
|
(includePadding ? tabSwitcher.getPaddingLeft() + tabSwitcher.getPaddingRight() :
|
||||||
|
0) - (tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ?
|
||||||
|
stackedTabCount * stackedTabSpacing : 0);
|
||||||
|
return targetWidth / width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setScale(@NonNull final Axis axis, @NonNull final View view,
|
||||||
|
final float scale) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
view.setScaleY(scale);
|
||||||
|
} else {
|
||||||
|
view.setScaleX(scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void animateScale(@NonNull final Axis axis,
|
||||||
|
@NonNull final ViewPropertyAnimator animator,
|
||||||
|
final float scale) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(animator, "The animator may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
animator.scaleY(scale);
|
||||||
|
} else {
|
||||||
|
animator.scaleX(scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getSize(@NonNull final Axis axis, @NonNull final View view) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
return view.getHeight() * getScale(view, false);
|
||||||
|
} else {
|
||||||
|
return view.getWidth() * getScale(view, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getTabContainerSize(@NonNull final Axis axis) {
|
||||||
|
return getTabContainerSize(axis, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getTabContainerSize(@NonNull final Axis axis, final boolean includePadding) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ViewGroup tabContainer = tabSwitcher.getTabContainer();
|
||||||
|
assert tabContainer != null;
|
||||||
|
FrameLayout.LayoutParams layoutParams =
|
||||||
|
(FrameLayout.LayoutParams) tabContainer.getLayoutParams();
|
||||||
|
int padding = !includePadding ? (getPadding(axis, Gravity.START, tabSwitcher) +
|
||||||
|
getPadding(axis, Gravity.END, tabSwitcher)) : 0;
|
||||||
|
Toolbar[] toolbars = tabSwitcher.getToolbars();
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
int toolbarSize =
|
||||||
|
!includePadding && tabSwitcher.areToolbarsShown() && toolbars != null ?
|
||||||
|
toolbars[0].getHeight() - tabInset : 0;
|
||||||
|
return tabContainer.getHeight() - layoutParams.topMargin - layoutParams.bottomMargin -
|
||||||
|
padding - toolbarSize;
|
||||||
|
} else {
|
||||||
|
return tabContainer.getWidth() - layoutParams.leftMargin - layoutParams.rightMargin -
|
||||||
|
padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getPivot(@NonNull final Axis axis, @NonNull final View view,
|
||||||
|
@NonNull final DragState dragState) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
ensureNotNull(dragState, "The drag state may not be null");
|
||||||
|
|
||||||
|
if (dragState == DragState.SWIPE) {
|
||||||
|
return getPivotWhenSwiping(axis, view);
|
||||||
|
} else if (dragState == DragState.OVERSHOOT_START) {
|
||||||
|
return getPivotWhenOvershootingAtStart(axis, view);
|
||||||
|
} else if (dragState == DragState.OVERSHOOT_END) {
|
||||||
|
return getPivotWhenOvershootingAtEnd(axis, view);
|
||||||
|
} else {
|
||||||
|
return getDefaultPivot(axis, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setPivot(@NonNull final Axis axis, @NonNull final View view,
|
||||||
|
final float pivot) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view.getLayoutParams();
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
float newPivot = pivot - layoutParams.topMargin - tabTitleContainerHeight;
|
||||||
|
view.setTranslationY(view.getTranslationY() +
|
||||||
|
(view.getPivotY() - newPivot) * (1 - view.getScaleY()));
|
||||||
|
view.setPivotY(newPivot);
|
||||||
|
} else {
|
||||||
|
float newPivot = pivot - layoutParams.leftMargin;
|
||||||
|
view.setTranslationX(view.getTranslationX() +
|
||||||
|
(view.getPivotX() - newPivot) * (1 - view.getScaleX()));
|
||||||
|
view.setPivotX(newPivot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final float getRotation(@NonNull final Axis axis, @NonNull final View view) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
return view.getRotationY();
|
||||||
|
} else {
|
||||||
|
return view.getRotationX();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setRotation(@NonNull final Axis axis, @NonNull final View view,
|
||||||
|
final float angle) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(view, "The view may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
view.setRotationY(
|
||||||
|
tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ? -1 * angle : angle);
|
||||||
|
} else {
|
||||||
|
view.setRotationX(
|
||||||
|
tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ? -1 * angle : angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void animateRotation(@NonNull final Axis axis,
|
||||||
|
@NonNull final ViewPropertyAnimator animator,
|
||||||
|
final float angle) {
|
||||||
|
ensureNotNull(axis, "The axis may not be null");
|
||||||
|
ensureNotNull(animator, "The animator may not be null");
|
||||||
|
|
||||||
|
if (getOrientationInvariantAxis(axis) == Axis.DRAGGING_AXIS) {
|
||||||
|
animator.rotationY(
|
||||||
|
tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ? -1 * angle : angle);
|
||||||
|
} else {
|
||||||
|
animator.rotationX(
|
||||||
|
tabSwitcher.getLayout() == Layout.PHONE_LANDSCAPE ? -1 * angle : angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout.phone;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Layout;
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.iterator.AbstractTabItemIterator;
|
||||||
|
import de.mrapp.android.tabswitcher.iterator.TabItemIterator;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.AbstractDragHandler;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.Arithmetics;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.Arithmetics.Axis;
|
||||||
|
import de.mrapp.android.tabswitcher.model.State;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.util.gesture.DragHelper;
|
||||||
|
import de.mrapp.android.util.view.AttachedViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A drag handler, which allows to calculate the position and state of tabs on touch events, when
|
||||||
|
* using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class PhoneDragHandler extends AbstractDragHandler<PhoneDragHandler.Callback> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified about the events of a drag handler,
|
||||||
|
* must implement.
|
||||||
|
*/
|
||||||
|
public interface Callback extends AbstractDragHandler.Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when tabs are overshooting at the start.
|
||||||
|
*
|
||||||
|
* @param position
|
||||||
|
* The position of the first tab in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void onStartOvershoot(float position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tabs should be tilted when overshooting at the
|
||||||
|
* start.
|
||||||
|
*
|
||||||
|
* @param angle
|
||||||
|
* The angle, the tabs should be tilted by, in degrees as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void onTiltOnStartOvershoot(float angle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tabs should be tilted when overshooting at the
|
||||||
|
* end.
|
||||||
|
*
|
||||||
|
* @param angle
|
||||||
|
* The angle, the tabs should be tilted by, in degrees as a {@link Float} value
|
||||||
|
*/
|
||||||
|
void onTiltOnEndOvershoot(float angle);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the tabs,
|
||||||
|
* whose positions and states are calculated by the drag handler.
|
||||||
|
*/
|
||||||
|
private final AttachedViewRecycler<TabItem, ?> viewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag helper, which is used to recognize drag gestures when overshooting.
|
||||||
|
*/
|
||||||
|
private final DragHelper overshootDragHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum overshoot distance in pixels.
|
||||||
|
*/
|
||||||
|
private final int maxOvershootDistance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum angle, tabs can be rotated by, when overshooting at the start, in degrees.
|
||||||
|
*/
|
||||||
|
private final float maxStartOvershootAngle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum angle, tabs can be rotated by, when overshooting at the end, in degrees.
|
||||||
|
*/
|
||||||
|
private final float maxEndOvershootAngle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of tabs, which are contained by a stack.
|
||||||
|
*/
|
||||||
|
private final int stackedTabCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The inset of tabs in pixels.
|
||||||
|
*/
|
||||||
|
private final int tabInset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that tabs are overshooting at the start.
|
||||||
|
*
|
||||||
|
* @param position
|
||||||
|
* The position of the first tab in pixels as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private void notifyOnStartOvershoot(final float position) {
|
||||||
|
if (getCallback() != null) {
|
||||||
|
getCallback().onStartOvershoot(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that the tabs should be tilted when overshooting at the start.
|
||||||
|
*
|
||||||
|
* @param angle
|
||||||
|
* The angle, the tabs should be tilted by, in degrees as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private void notifyOnTiltOnStartOvershoot(final float angle) {
|
||||||
|
if (getCallback() != null) {
|
||||||
|
getCallback().onTiltOnStartOvershoot(angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the callback, that the tabs should be titled when overshooting at the end.
|
||||||
|
*
|
||||||
|
* @param angle
|
||||||
|
* The angle, the tabs should be tilted by, in degrees as a {@link Float} value
|
||||||
|
*/
|
||||||
|
private void notifyOnTiltOnEndOvershoot(final float angle) {
|
||||||
|
if (getCallback() != null) {
|
||||||
|
getCallback().onTiltOnEndOvershoot(angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new drag handler, which allows to calculate the position and state of tabs on touch
|
||||||
|
* events, when using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, whose tabs' positions and states should be calculated by the drag
|
||||||
|
* handler, as an instance of the class {@link TabSwitcher}. The tab switcher may not be
|
||||||
|
* null
|
||||||
|
* @param arithmetics
|
||||||
|
* The arithmetics, which should be used to calculate the position, size and rotation of
|
||||||
|
* tabs, as an instance of the type {@link Arithmetics}. The arithmetics may not be
|
||||||
|
* null
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which allows to inflate the views, which are used to visualize the
|
||||||
|
* tabs, whose positions and states should be calculated by the tab switcher, as an
|
||||||
|
* instance of the class AttachedViewRecycler. The view recycler may not be null
|
||||||
|
*/
|
||||||
|
public PhoneDragHandler(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final Arithmetics arithmetics,
|
||||||
|
@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler) {
|
||||||
|
super(tabSwitcher, arithmetics, true);
|
||||||
|
ensureNotNull(viewRecycler, "The view recycler may not be null");
|
||||||
|
this.viewRecycler = viewRecycler;
|
||||||
|
this.overshootDragHelper = new DragHelper(0);
|
||||||
|
Resources resources = tabSwitcher.getResources();
|
||||||
|
this.tabInset = resources.getDimensionPixelSize(R.dimen.tab_inset);
|
||||||
|
this.stackedTabCount = resources.getInteger(R.integer.stacked_tab_count);
|
||||||
|
this.maxOvershootDistance = resources.getDimensionPixelSize(R.dimen.max_overshoot_distance);
|
||||||
|
this.maxStartOvershootAngle = resources.getInteger(R.integer.max_start_overshoot_angle);
|
||||||
|
this.maxEndOvershootAngle = resources.getInteger(R.integer.max_end_overshoot_angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
protected final TabItem getFocusedTab(final float position) {
|
||||||
|
AbstractTabItemIterator iterator =
|
||||||
|
new TabItemIterator.Builder(getTabSwitcher(), viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.getTag().getState() == State.FLOATING ||
|
||||||
|
tabItem.getTag().getState() == State.STACKED_START_ATOP) {
|
||||||
|
View view = tabItem.getView();
|
||||||
|
Toolbar[] toolbars = getTabSwitcher().getToolbars();
|
||||||
|
float toolbarHeight = getTabSwitcher().getLayout() != Layout.PHONE_LANDSCAPE &&
|
||||||
|
getTabSwitcher().areToolbarsShown() && toolbars != null ?
|
||||||
|
toolbars[0].getHeight() - tabInset : 0;
|
||||||
|
float viewPosition =
|
||||||
|
getArithmetics().getPosition(Axis.DRAGGING_AXIS, view) + toolbarHeight +
|
||||||
|
getArithmetics().getPadding(Axis.DRAGGING_AXIS, Gravity.START,
|
||||||
|
getTabSwitcher());
|
||||||
|
|
||||||
|
if (viewPosition <= position) {
|
||||||
|
return tabItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final float onOvershootStart(final float dragPosition,
|
||||||
|
final float overshootThreshold) {
|
||||||
|
float result = overshootThreshold;
|
||||||
|
overshootDragHelper.update(dragPosition);
|
||||||
|
float overshootDistance = overshootDragHelper.getDragDistance();
|
||||||
|
|
||||||
|
if (overshootDistance < 0) {
|
||||||
|
float absOvershootDistance = Math.abs(overshootDistance);
|
||||||
|
float startOvershootDistance =
|
||||||
|
getTabSwitcher().getCount() >= stackedTabCount ? maxOvershootDistance :
|
||||||
|
(getTabSwitcher().getCount() > 1 ? (float) maxOvershootDistance /
|
||||||
|
(float) getTabSwitcher().getCount() : 0);
|
||||||
|
|
||||||
|
if (absOvershootDistance <= startOvershootDistance) {
|
||||||
|
float ratio =
|
||||||
|
Math.max(0, Math.min(1, absOvershootDistance / startOvershootDistance));
|
||||||
|
AbstractTabItemIterator iterator =
|
||||||
|
new TabItemIterator.Builder(getTabSwitcher(), viewRecycler).create();
|
||||||
|
TabItem tabItem = iterator.getItem(0);
|
||||||
|
float currentPosition = tabItem.getTag().getPosition();
|
||||||
|
float position = currentPosition - (currentPosition * ratio);
|
||||||
|
notifyOnStartOvershoot(position);
|
||||||
|
} else {
|
||||||
|
float ratio =
|
||||||
|
(absOvershootDistance - startOvershootDistance) / maxOvershootDistance;
|
||||||
|
|
||||||
|
if (ratio >= 1) {
|
||||||
|
overshootDragHelper.setMinDragDistance(overshootDistance);
|
||||||
|
result = dragPosition + maxOvershootDistance + startOvershootDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyOnTiltOnStartOvershoot(
|
||||||
|
Math.max(0, Math.min(1, ratio)) * maxStartOvershootAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final float onOvershootEnd(final float dragPosition, final float overshootThreshold) {
|
||||||
|
float result = overshootThreshold;
|
||||||
|
overshootDragHelper.update(dragPosition);
|
||||||
|
float overshootDistance = overshootDragHelper.getDragDistance();
|
||||||
|
float ratio = overshootDistance / maxOvershootDistance;
|
||||||
|
|
||||||
|
if (ratio >= 1) {
|
||||||
|
overshootDragHelper.setMaxDragDistance(overshootDistance);
|
||||||
|
result = dragPosition - maxOvershootDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyOnTiltOnEndOvershoot(Math.max(0, Math.min(1, ratio)) *
|
||||||
|
-(getTabSwitcher().getCount() > 1 ? maxEndOvershootAngle : maxStartOvershootAngle));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void onOvershootReverted() {
|
||||||
|
overshootDragHelper.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void onReset() {
|
||||||
|
overshootDragHelper.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final boolean isSwipeThresholdReached(@NonNull final TabItem swipedTabItem) {
|
||||||
|
View view = swipedTabItem.getView();
|
||||||
|
return Math.abs(getArithmetics().getPosition(Axis.ORTHOGONAL_AXIS, view)) >
|
||||||
|
getArithmetics().getTabContainerSize(Axis.ORTHOGONAL_AXIS) / 6f;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,836 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout.phone;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.MenuRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v4.util.Pair;
|
||||||
|
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout.LayoutParams;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Animation;
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.TabCloseListener;
|
||||||
|
import de.mrapp.android.tabswitcher.TabPreviewListener;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcherDecorator;
|
||||||
|
import de.mrapp.android.tabswitcher.iterator.AbstractTabItemIterator;
|
||||||
|
import de.mrapp.android.tabswitcher.iterator.TabItemIterator;
|
||||||
|
import de.mrapp.android.tabswitcher.model.Model;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabSwitcherModel;
|
||||||
|
import de.mrapp.android.util.ViewUtil;
|
||||||
|
import de.mrapp.android.util.logging.LogLevel;
|
||||||
|
import de.mrapp.android.util.multithreading.AbstractDataBinder;
|
||||||
|
import de.mrapp.android.util.view.AbstractViewRecycler;
|
||||||
|
import de.mrapp.android.util.view.AttachedViewRecycler;
|
||||||
|
import de.mrapp.android.util.view.ViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view recycler adapter, which allows to inflate the views, which are used to visualize the tabs
|
||||||
|
* of a {@link TabSwitcher}, when using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class PhoneRecyclerAdapter extends AbstractViewRecycler.Adapter<TabItem, Integer>
|
||||||
|
implements Tab.Callback, Model.Listener,
|
||||||
|
AbstractDataBinder.Listener<Bitmap, Tab, ImageView, TabItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab switcher, the tabs belong to.
|
||||||
|
*/
|
||||||
|
private final TabSwitcher tabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model, which belongs to the tab switcher.
|
||||||
|
*/
|
||||||
|
private final TabSwitcherModel model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which allows to inflate the child views of tabs.
|
||||||
|
*/
|
||||||
|
private final ViewRecycler<Tab, Void> childViewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data binder, which allows to render previews of tabs.
|
||||||
|
*/
|
||||||
|
private final AbstractDataBinder<Bitmap, Tab, ImageView, TabItem> dataBinder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The inset of tabs in pixels.
|
||||||
|
*/
|
||||||
|
private final int tabInset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width of the border, which is shown around the preview of tabs, in pixels.
|
||||||
|
*/
|
||||||
|
private final int tabBorderWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The height of the view group, which contains a tab's title and close button, in pixels.
|
||||||
|
*/
|
||||||
|
private final int tabTitleContainerHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default background color of tabs.
|
||||||
|
*/
|
||||||
|
private final int tabBackgroundColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default text color of a tab's title.
|
||||||
|
*/
|
||||||
|
private final int tabTitleTextColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, the adapter is bound to.
|
||||||
|
*/
|
||||||
|
private AttachedViewRecycler<TabItem, Integer> viewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inflates the child view of a tab and adds it to the view hierarchy.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the tab, whose child view should be inflated, as
|
||||||
|
* an instance of the class {@link TabItem}. The tab item may not be null
|
||||||
|
*/
|
||||||
|
private void addChildView(@NonNull final TabItem tabItem) {
|
||||||
|
PhoneTabViewHolder viewHolder = tabItem.getViewHolder();
|
||||||
|
View view = viewHolder.child;
|
||||||
|
Tab tab = tabItem.getTab();
|
||||||
|
|
||||||
|
if (view == null) {
|
||||||
|
ViewGroup parent = viewHolder.childContainer;
|
||||||
|
Pair<View, ?> pair = childViewRecycler.inflate(tab, parent);
|
||||||
|
view = pair.first;
|
||||||
|
LayoutParams layoutParams =
|
||||||
|
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||||
|
layoutParams.setMargins(model.getPaddingLeft(), model.getPaddingTop(),
|
||||||
|
model.getPaddingRight(), model.getPaddingBottom());
|
||||||
|
parent.addView(view, 0, layoutParams);
|
||||||
|
viewHolder.child = view;
|
||||||
|
} else {
|
||||||
|
childViewRecycler.getAdapter().onShowView(model.getContext(), view, tab, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewHolder.previewImageView.setVisibility(View.GONE);
|
||||||
|
viewHolder.previewImageView.setImageBitmap(null);
|
||||||
|
viewHolder.borderView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders and displays the child view of a tab.
|
||||||
|
*
|
||||||
|
* @param tabItem
|
||||||
|
* The tab item, which corresponds to the tab, whose preview should be rendered, as an
|
||||||
|
* instance of the class {@link TabItem}. The tab item may not be null
|
||||||
|
*/
|
||||||
|
private void renderChildView(@NonNull final TabItem tabItem) {
|
||||||
|
Tab tab = tabItem.getTab();
|
||||||
|
PhoneTabViewHolder viewHolder = tabItem.getViewHolder();
|
||||||
|
viewHolder.borderView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
if (viewHolder.child != null) {
|
||||||
|
childViewRecycler.getAdapter().onRemoveView(viewHolder.child, tab);
|
||||||
|
dataBinder.load(tab, viewHolder.previewImageView, false, tabItem);
|
||||||
|
removeChildView(viewHolder, tab);
|
||||||
|
} else {
|
||||||
|
dataBinder.load(tab, viewHolder.previewImageView, tabItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the child of a tab from its parent.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose child should be removed, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
*/
|
||||||
|
private void removeChildView(@NonNull final PhoneTabViewHolder viewHolder,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
if (viewHolder.childContainer.getChildCount() > 2) {
|
||||||
|
viewHolder.childContainer.removeViewAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewHolder.child = null;
|
||||||
|
childViewRecycler.remove(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the log level.
|
||||||
|
*/
|
||||||
|
private void adaptLogLevel() {
|
||||||
|
dataBinder.setLogLevel(model.getLogLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the title of a tab.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose title should be adapted, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptTitle(@NonNull final PhoneTabViewHolder viewHolder, @NonNull final Tab tab) {
|
||||||
|
viewHolder.titleTextView.setText(tab.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the icon of a tab.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The icon, whose icon should be adapted, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptIcon(@NonNull final PhoneTabViewHolder viewHolder, @NonNull final Tab tab) {
|
||||||
|
Drawable icon = tab.getIcon(model.getContext());
|
||||||
|
viewHolder.titleTextView
|
||||||
|
.setCompoundDrawablesWithIntrinsicBounds(icon != null ? icon : model.getTabIcon(),
|
||||||
|
null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the visibility of a tab's close button.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The icon, whose close button should be adapted, as an instance of the class {@link
|
||||||
|
* Tab}. The tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptCloseButton(@NonNull final PhoneTabViewHolder viewHolder,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
viewHolder.closeButton.setVisibility(tab.isCloseable() ? View.VISIBLE : View.GONE);
|
||||||
|
viewHolder.closeButton.setOnClickListener(
|
||||||
|
tab.isCloseable() ? createCloseButtonClickListener(viewHolder.closeButton, tab) :
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the icon of a tab's close button.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The icon, whose icon hould be adapted, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptCloseButtonIcon(@NonNull final PhoneTabViewHolder viewHolder,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
Drawable icon = tab.getCloseButtonIcon(model.getContext());
|
||||||
|
|
||||||
|
if (icon == null) {
|
||||||
|
icon = model.getTabCloseButtonIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon != null) {
|
||||||
|
viewHolder.closeButton.setImageDrawable(icon);
|
||||||
|
} else {
|
||||||
|
viewHolder.closeButton.setImageResource(R.drawable.ic_close_tab_18dp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns a listener, which allows to close a specific tab, when its close button
|
||||||
|
* is clicked.
|
||||||
|
*
|
||||||
|
* @param closeButton
|
||||||
|
* The tab's close button as an instance of the class {@link ImageButton}. The button
|
||||||
|
* may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be closed, as an instance of the class {@link Tab}. The tab may
|
||||||
|
* not be null
|
||||||
|
* @return The listener, which has been created, as an instance of the class {@link
|
||||||
|
* OnClickListener}. The listener may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private OnClickListener createCloseButtonClickListener(@NonNull final ImageButton closeButton,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
return new OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(final View v) {
|
||||||
|
if (notifyOnCloseTab(tab)) {
|
||||||
|
closeButton.setOnClickListener(null);
|
||||||
|
tabSwitcher.removeTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all listeners, that a tab is about to be closed by clicking its close button.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which is about to be closed, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
* @return True, if the tab should be closed, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean notifyOnCloseTab(@NonNull final Tab tab) {
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
for (TabCloseListener listener : model.getTabCloseListeners()) {
|
||||||
|
result &= listener.onCloseTab(tabSwitcher, tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the background color of a tab.
|
||||||
|
*
|
||||||
|
* @param view
|
||||||
|
* The view, which is used to visualize the tab, as an instance of the class {@link
|
||||||
|
* View}. The view may not be null
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose background color should be adapted, as an instance of the class {@link
|
||||||
|
* Tab}. The tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptBackgroundColor(@NonNull final View view,
|
||||||
|
@NonNull final PhoneTabViewHolder viewHolder,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
ColorStateList colorStateList =
|
||||||
|
tab.getBackgroundColor() != null ? tab.getBackgroundColor() :
|
||||||
|
model.getTabBackgroundColor();
|
||||||
|
int color = tabBackgroundColor;
|
||||||
|
|
||||||
|
if (colorStateList != null) {
|
||||||
|
int[] stateSet =
|
||||||
|
model.getSelectedTab() == tab ? new int[]{android.R.attr.state_selected} :
|
||||||
|
new int[]{};
|
||||||
|
color = colorStateList.getColorForState(stateSet, colorStateList.getDefaultColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
Drawable background = view.getBackground();
|
||||||
|
background.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
|
||||||
|
Drawable border = viewHolder.borderView.getBackground();
|
||||||
|
border.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the text color of a tab's title.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose text color should be adapted, as an instance of the class {@link Tab}.
|
||||||
|
* The tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptTitleTextColor(@NonNull final PhoneTabViewHolder viewHolder,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
ColorStateList colorStateList = tab.getTitleTextColor() != null ? tab.getTitleTextColor() :
|
||||||
|
model.getTabTitleTextColor();
|
||||||
|
|
||||||
|
if (colorStateList != null) {
|
||||||
|
viewHolder.titleTextView.setTextColor(colorStateList);
|
||||||
|
} else {
|
||||||
|
viewHolder.titleTextView.setTextColor(tabTitleTextColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the selection state of a tab's views.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose selection state should be adapted, as an instance of the class {@link
|
||||||
|
* Tab}. The tab may not be null
|
||||||
|
*/
|
||||||
|
private void adaptSelectionState(@NonNull final PhoneTabViewHolder viewHolder,
|
||||||
|
@NonNull final Tab tab) {
|
||||||
|
boolean selected = model.getSelectedTab() == tab;
|
||||||
|
viewHolder.titleTextView.setSelected(selected);
|
||||||
|
viewHolder.closeButton.setSelected(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the appearance of all currently inflated tabs, depending on whether they are currently
|
||||||
|
* selected, or not.
|
||||||
|
*/
|
||||||
|
private void adaptAllSelectionStates() {
|
||||||
|
AbstractTabItemIterator iterator =
|
||||||
|
new TabItemIterator.Builder(model, viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
Tab tab = tabItem.getTab();
|
||||||
|
PhoneTabViewHolder viewHolder = tabItem.getViewHolder();
|
||||||
|
adaptSelectionState(viewHolder, tab);
|
||||||
|
adaptBackgroundColor(tabItem.getView(), viewHolder, tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts the padding of a tab.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which stores references to the tab's views, as an instance of the
|
||||||
|
* class {@link PhoneTabViewHolder}. The view holder may not be null
|
||||||
|
*/
|
||||||
|
private void adaptPadding(@NonNull final PhoneTabViewHolder viewHolder) {
|
||||||
|
if (viewHolder.child != null) {
|
||||||
|
LayoutParams childLayoutParams = (LayoutParams) viewHolder.child.getLayoutParams();
|
||||||
|
childLayoutParams.setMargins(model.getPaddingLeft(), model.getPaddingTop(),
|
||||||
|
model.getPaddingRight(), model.getPaddingBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutParams previewLayoutParams =
|
||||||
|
(LayoutParams) viewHolder.previewImageView.getLayoutParams();
|
||||||
|
previewLayoutParams
|
||||||
|
.setMargins(model.getPaddingLeft(), model.getPaddingTop(), model.getPaddingRight(),
|
||||||
|
model.getPaddingBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab item, which corresponds to a specific tab.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose tab item should be returned, as an instance of the class {@link Tab}.
|
||||||
|
* The tab may not be null
|
||||||
|
* @return The tab item, which corresponds to the given tab, as an instance of the class {@link
|
||||||
|
* TabItem} or null, if no view, which visualizes the tab, is currently inflated
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private TabItem getTabItem(@NonNull final Tab tab) {
|
||||||
|
ensureNotNull(viewRecycler, "No view recycler has been set", IllegalStateException.class);
|
||||||
|
int index = model.indexOf(tab);
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
TabItem tabItem = TabItem.create(model, viewRecycler, index);
|
||||||
|
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
return tabItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new view recycler adapter, which allows to inflate the views, which are used to
|
||||||
|
* visualize the tabs of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher as an instance of the class {@link TabSwitcher}. The tab switcher
|
||||||
|
* may not be null
|
||||||
|
* @param model
|
||||||
|
* The model, which belongs to the tab switcher, as an instance of the class {@link
|
||||||
|
* TabSwitcherModel}. The model may not be null
|
||||||
|
* @param childViewRecycler
|
||||||
|
* The view recycler, which allows to inflate the child views of tabs, as an instance of
|
||||||
|
* the class ViewRecycler. The view recycler may not be null
|
||||||
|
*/
|
||||||
|
public PhoneRecyclerAdapter(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final TabSwitcherModel model,
|
||||||
|
@NonNull final ViewRecycler<Tab, Void> childViewRecycler) {
|
||||||
|
ensureNotNull(tabSwitcher, "The tab switcher may not be null");
|
||||||
|
ensureNotNull(model, "The model may not be null");
|
||||||
|
ensureNotNull(childViewRecycler, "The child view recycler may not be null");
|
||||||
|
this.tabSwitcher = tabSwitcher;
|
||||||
|
this.model = model;
|
||||||
|
this.childViewRecycler = childViewRecycler;
|
||||||
|
this.dataBinder = new PreviewDataBinder(tabSwitcher, childViewRecycler);
|
||||||
|
this.dataBinder.addListener(this);
|
||||||
|
Resources resources = tabSwitcher.getResources();
|
||||||
|
this.tabInset = resources.getDimensionPixelSize(R.dimen.tab_inset);
|
||||||
|
this.tabBorderWidth = resources.getDimensionPixelSize(R.dimen.tab_border_width);
|
||||||
|
this.tabTitleContainerHeight =
|
||||||
|
resources.getDimensionPixelSize(R.dimen.tab_title_container_height);
|
||||||
|
this.tabBackgroundColor =
|
||||||
|
ContextCompat.getColor(tabSwitcher.getContext(), R.color.tab_background_color);
|
||||||
|
this.tabTitleTextColor =
|
||||||
|
ContextCompat.getColor(tabSwitcher.getContext(), R.color.tab_title_text_color);
|
||||||
|
this.viewRecycler = null;
|
||||||
|
adaptLogLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the view recycler, which allows to inflate the views, which are used to visualize tabs.
|
||||||
|
*
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which should be set, as an instance of the class
|
||||||
|
* AttachedViewRecycler. The view recycler may not be null
|
||||||
|
*/
|
||||||
|
public final void setViewRecycler(
|
||||||
|
@NonNull final AttachedViewRecycler<TabItem, Integer> viewRecycler) {
|
||||||
|
ensureNotNull(viewRecycler, "The view recycler may not be null");
|
||||||
|
this.viewRecycler = viewRecycler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all previously rendered previews from the cache.
|
||||||
|
*/
|
||||||
|
public final void clearCachedPreviews() {
|
||||||
|
dataBinder.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final View onInflateView(@NonNull final LayoutInflater inflater,
|
||||||
|
@Nullable final ViewGroup parent,
|
||||||
|
@NonNull final TabItem tabItem, final int viewType,
|
||||||
|
@NonNull final Integer... params) {
|
||||||
|
PhoneTabViewHolder viewHolder = new PhoneTabViewHolder();
|
||||||
|
View view = inflater.inflate(R.layout.phone_tab, tabSwitcher.getTabContainer(), false);
|
||||||
|
Drawable backgroundDrawable =
|
||||||
|
ContextCompat.getDrawable(model.getContext(), R.drawable.phone_tab_background);
|
||||||
|
ViewUtil.setBackground(view, backgroundDrawable);
|
||||||
|
int padding = tabInset + tabBorderWidth;
|
||||||
|
view.setPadding(padding, tabInset, padding, padding);
|
||||||
|
viewHolder.titleContainer = (ViewGroup) view.findViewById(R.id.tab_title_container);
|
||||||
|
viewHolder.titleTextView = (TextView) view.findViewById(R.id.tab_title_text_view);
|
||||||
|
viewHolder.closeButton = (ImageButton) view.findViewById(R.id.close_tab_button);
|
||||||
|
viewHolder.childContainer = (ViewGroup) view.findViewById(R.id.child_container);
|
||||||
|
viewHolder.previewImageView = (ImageView) view.findViewById(R.id.preview_image_view);
|
||||||
|
adaptPadding(viewHolder);
|
||||||
|
viewHolder.borderView = view.findViewById(R.id.border_view);
|
||||||
|
Drawable borderDrawable =
|
||||||
|
ContextCompat.getDrawable(model.getContext(), R.drawable.phone_tab_border);
|
||||||
|
ViewUtil.setBackground(viewHolder.borderView, borderDrawable);
|
||||||
|
view.setTag(R.id.tag_view_holder, viewHolder);
|
||||||
|
tabItem.setView(view);
|
||||||
|
tabItem.setViewHolder(viewHolder);
|
||||||
|
view.setTag(R.id.tag_properties, tabItem.getTag());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onShowView(@NonNull final Context context, @NonNull final View view,
|
||||||
|
@NonNull final TabItem tabItem, final boolean inflated,
|
||||||
|
@NonNull final Integer... params) {
|
||||||
|
PhoneTabViewHolder viewHolder = (PhoneTabViewHolder) view.getTag(R.id.tag_view_holder);
|
||||||
|
|
||||||
|
if (!tabItem.isInflated()) {
|
||||||
|
tabItem.setView(view);
|
||||||
|
tabItem.setViewHolder(viewHolder);
|
||||||
|
view.setTag(R.id.tag_properties, tabItem.getTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutParams layoutParams =
|
||||||
|
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||||
|
int borderMargin = -(tabInset + tabBorderWidth);
|
||||||
|
int bottomMargin = params.length > 0 && params[0] != -1 ? params[0] : borderMargin;
|
||||||
|
layoutParams.leftMargin = borderMargin;
|
||||||
|
layoutParams.topMargin = -(tabInset + tabTitleContainerHeight);
|
||||||
|
layoutParams.rightMargin = borderMargin;
|
||||||
|
layoutParams.bottomMargin = bottomMargin;
|
||||||
|
view.setLayoutParams(layoutParams);
|
||||||
|
Tab tab = tabItem.getTab();
|
||||||
|
tab.addCallback(this);
|
||||||
|
adaptTitle(viewHolder, tab);
|
||||||
|
adaptIcon(viewHolder, tab);
|
||||||
|
adaptCloseButton(viewHolder, tab);
|
||||||
|
adaptCloseButtonIcon(viewHolder, tab);
|
||||||
|
adaptBackgroundColor(view, viewHolder, tab);
|
||||||
|
adaptTitleTextColor(viewHolder, tab);
|
||||||
|
adaptSelectionState(viewHolder, tab);
|
||||||
|
|
||||||
|
if (!model.isSwitcherShown()) {
|
||||||
|
if (tab == model.getSelectedTab()) {
|
||||||
|
addChildView(tabItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderChildView(tabItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onRemoveView(@NonNull final View view, @NonNull final TabItem tabItem) {
|
||||||
|
PhoneTabViewHolder viewHolder = (PhoneTabViewHolder) view.getTag(R.id.tag_view_holder);
|
||||||
|
Tab tab = tabItem.getTab();
|
||||||
|
tab.removeCallback(this);
|
||||||
|
removeChildView(viewHolder, tab);
|
||||||
|
|
||||||
|
if (!dataBinder.isCached(tab)) {
|
||||||
|
Drawable drawable = viewHolder.previewImageView.getDrawable();
|
||||||
|
viewHolder.previewImageView.setImageBitmap(null);
|
||||||
|
|
||||||
|
if (drawable instanceof BitmapDrawable) {
|
||||||
|
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
|
||||||
|
|
||||||
|
if (bitmap != null && !bitmap.isRecycled()) {
|
||||||
|
bitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewHolder.previewImageView.setImageBitmap(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setTag(R.id.tag_properties, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTitleChanged(@NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = getTabItem(tab);
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
adaptTitle(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onIconChanged(@NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = getTabItem(tab);
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
adaptIcon(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onCloseableChanged(@NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = getTabItem(tab);
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
adaptCloseButton(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onCloseButtonIconChanged(@NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = getTabItem(tab);
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
adaptCloseButtonIcon(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onBackgroundColorChanged(@NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = getTabItem(tab);
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
adaptBackgroundColor(tabItem.getView(), tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTitleTextColorChanged(@NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = getTabItem(tab);
|
||||||
|
|
||||||
|
if (tabItem != null) {
|
||||||
|
adaptTitleTextColor(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onLogLevelChanged(@NonNull final LogLevel logLevel) {
|
||||||
|
adaptLogLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onDecoratorChanged(@NonNull final TabSwitcherDecorator decorator) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSwitcherShown() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSwitcherHidden() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSelectionChanged(final int previousIndex, final int index,
|
||||||
|
@Nullable final Tab selectedTab,
|
||||||
|
final boolean switcherHidden) {
|
||||||
|
adaptAllSelectionStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabAdded(final int index, @NonNull final Tab tab,
|
||||||
|
final int previousSelectedTabIndex, final int selectedTabIndex,
|
||||||
|
final boolean switcherVisibilityChanged,
|
||||||
|
@NonNull final Animation animation) {
|
||||||
|
if (previousSelectedTabIndex != selectedTabIndex) {
|
||||||
|
adaptAllSelectionStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onAllTabsAdded(final int index, @NonNull final Tab[] tabs,
|
||||||
|
final int previousSelectedTabIndex, final int selectedTabIndex,
|
||||||
|
@NonNull final Animation animation) {
|
||||||
|
if (previousSelectedTabIndex != selectedTabIndex) {
|
||||||
|
adaptAllSelectionStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabRemoved(final int index, @NonNull final Tab tab,
|
||||||
|
final int previousSelectedTabIndex, final int selectedTabIndex,
|
||||||
|
@NonNull final Animation animation) {
|
||||||
|
if (previousSelectedTabIndex != selectedTabIndex) {
|
||||||
|
adaptAllSelectionStates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onAllTabsRemoved(@NonNull final Tab[] tabs,
|
||||||
|
@NonNull final Animation animation) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onPaddingChanged(final int left, final int top, final int right,
|
||||||
|
final int bottom) {
|
||||||
|
TabItemIterator iterator = new TabItemIterator.Builder(model, viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
adaptPadding(tabItem.getViewHolder());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabIconChanged(@Nullable final Drawable icon) {
|
||||||
|
TabItemIterator iterator = new TabItemIterator.Builder(model, viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
adaptIcon(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabBackgroundColorChanged(@Nullable final ColorStateList colorStateList) {
|
||||||
|
TabItemIterator iterator = new TabItemIterator.Builder(model, viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
adaptBackgroundColor(tabItem.getView(), tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabTitleColorChanged(@Nullable final ColorStateList colorStateList) {
|
||||||
|
TabItemIterator iterator = new TabItemIterator.Builder(model, viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
adaptTitleTextColor(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabCloseButtonIconChanged(@Nullable final Drawable icon) {
|
||||||
|
TabItemIterator iterator = new TabItemIterator.Builder(model, viewRecycler).create();
|
||||||
|
TabItem tabItem;
|
||||||
|
|
||||||
|
while ((tabItem = iterator.next()) != null) {
|
||||||
|
if (tabItem.isInflated()) {
|
||||||
|
adaptCloseButtonIcon(tabItem.getViewHolder(), tabItem.getTab());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarVisibilityChanged(final boolean visible) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarTitleChanged(@Nullable final CharSequence title) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarNavigationIconChanged(@Nullable final Drawable icon,
|
||||||
|
@Nullable final OnClickListener listener) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onToolbarMenuInflated(@MenuRes final int resourceId,
|
||||||
|
@Nullable final OnMenuItemClickListener listener) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean onLoadData(
|
||||||
|
@NonNull final AbstractDataBinder<Bitmap, Tab, ImageView, TabItem> dataBinder,
|
||||||
|
@NonNull final Tab key, @NonNull final TabItem... params) {
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
for (TabPreviewListener listener : model.getTabPreviewListeners()) {
|
||||||
|
result &= listener.onLoadTabPreview(tabSwitcher, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onFinished(
|
||||||
|
@NonNull final AbstractDataBinder<Bitmap, Tab, ImageView, TabItem> dataBinder,
|
||||||
|
@NonNull final Tab key, @Nullable final Bitmap data, @NonNull final ImageView view,
|
||||||
|
@NonNull final TabItem... params) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onCanceled(
|
||||||
|
@NonNull final AbstractDataBinder<Bitmap, Tab, ImageView, TabItem> dataBinder) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout.phone;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.AbstractTabViewHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view holder, which allows to store references to the views, a tab of a {@link TabSwitcher}
|
||||||
|
* consists of, when using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class PhoneTabViewHolder extends AbstractTabViewHolder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view group, which contains the title and close button of a tab.
|
||||||
|
*/
|
||||||
|
public ViewGroup titleContainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view group, which contains the child view of a tab.
|
||||||
|
*/
|
||||||
|
public ViewGroup childContainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The child view, which contains the tab's content.
|
||||||
|
*/
|
||||||
|
public View child;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The image view, which is used to display the preview of a tab.
|
||||||
|
*/
|
||||||
|
public ImageView previewImageView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view, which is used to display a border around the preview of a tab.
|
||||||
|
*/
|
||||||
|
public View borderView;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.layout.phone;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.util.LruCache;
|
||||||
|
import android.support.v4.util.Pair;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.MeasureSpec;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.model.TabItem;
|
||||||
|
import de.mrapp.android.util.multithreading.AbstractDataBinder;
|
||||||
|
import de.mrapp.android.util.view.ViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data binder, which allows to asynchronously render preview images of tabs and display them
|
||||||
|
* afterwards.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class PreviewDataBinder extends AbstractDataBinder<Bitmap, Tab, ImageView, TabItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parent view of the tab switcher, the tabs belong to.
|
||||||
|
*/
|
||||||
|
private final ViewGroup parent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view recycler, which is used to inflate child views.
|
||||||
|
*/
|
||||||
|
private final ViewRecycler<Tab, Void> childViewRecycler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new data binder, which allows to asynchronously render preview images of tabs and
|
||||||
|
* display them afterwards.
|
||||||
|
*
|
||||||
|
* @param parent
|
||||||
|
* The parent view of the tab switcher, the tabs belong to, as an instance of the class
|
||||||
|
* {@link ViewGroup}. The parent may not be null
|
||||||
|
* @param childViewRecycler
|
||||||
|
* The view recycler, which should be used to inflate child views, as an instance of the
|
||||||
|
* class ViewRecycler. The view recycler may not be null
|
||||||
|
*/
|
||||||
|
public PreviewDataBinder(@NonNull final ViewGroup parent,
|
||||||
|
@NonNull final ViewRecycler<Tab, Void> childViewRecycler) {
|
||||||
|
super(parent.getContext(), new LruCache<Tab, Bitmap>(7));
|
||||||
|
ensureNotNull(parent, "The parent may not be null");
|
||||||
|
ensureNotNull(childViewRecycler, "The child view recycler may not be null");
|
||||||
|
this.parent = parent;
|
||||||
|
this.childViewRecycler = childViewRecycler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void onPreExecute(@NonNull final ImageView view,
|
||||||
|
@NonNull final TabItem... params) {
|
||||||
|
TabItem tabItem = params[0];
|
||||||
|
PhoneTabViewHolder viewHolder = tabItem.getViewHolder();
|
||||||
|
View child = viewHolder.child;
|
||||||
|
Tab tab = tabItem.getTab();
|
||||||
|
|
||||||
|
if (child == null) {
|
||||||
|
Pair<View, ?> pair = childViewRecycler.inflate(tab, viewHolder.childContainer);
|
||||||
|
child = pair.first;
|
||||||
|
} else {
|
||||||
|
childViewRecycler.getAdapter().onShowView(getContext(), child, tab, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewHolder.child = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected final Bitmap doInBackground(@NonNull final Tab key,
|
||||||
|
@NonNull final TabItem... params) {
|
||||||
|
TabItem tabItem = params[0];
|
||||||
|
PhoneTabViewHolder viewHolder = tabItem.getViewHolder();
|
||||||
|
View child = viewHolder.child;
|
||||||
|
viewHolder.child = null;
|
||||||
|
int width = parent.getWidth();
|
||||||
|
int height = parent.getHeight();
|
||||||
|
child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
||||||
|
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
child.draw(canvas);
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void onPostExecute(@NonNull final ImageView view, @Nullable final Bitmap data,
|
||||||
|
@NonNull final TabItem... params) {
|
||||||
|
view.setImageBitmap(data);
|
||||||
|
view.setVisibility(data != null ? View.VISIBLE : View.GONE);
|
||||||
|
TabItem tabItem = params[0];
|
||||||
|
childViewRecycler.remove(tabItem.getTab());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
915
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/model/Model.java
Executable file
@ -0,0 +1,915 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.model;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.ColorInt;
|
||||||
|
import android.support.annotation.DrawableRes;
|
||||||
|
import android.support.annotation.MenuRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Animation;
|
||||||
|
import de.mrapp.android.tabswitcher.SwipeAnimation;
|
||||||
|
import de.mrapp.android.tabswitcher.SwipeAnimation.SwipeDirection;
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.TabCloseListener;
|
||||||
|
import de.mrapp.android.tabswitcher.TabPreviewListener;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcherDecorator;
|
||||||
|
import de.mrapp.android.util.logging.LogLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which implements the model of a {@link TabSwitcher} must
|
||||||
|
* implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface Model extends Iterable<Tab> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, which should be notified about the model's events, must
|
||||||
|
* implement.
|
||||||
|
*/
|
||||||
|
interface Listener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the log level has been changed.
|
||||||
|
*
|
||||||
|
* @param logLevel
|
||||||
|
* The log level, which has been set, as a value of the enum LogLevel. The log level
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
void onLogLevelChanged(@NonNull LogLevel logLevel);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the decorator has been changed.
|
||||||
|
*
|
||||||
|
* @param decorator
|
||||||
|
* The decorator, which has been set, as an instance of the class {@link
|
||||||
|
* TabSwitcherDecorator}. The decorator may not be null
|
||||||
|
*/
|
||||||
|
void onDecoratorChanged(@NonNull TabSwitcherDecorator decorator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab switcher has been shown.
|
||||||
|
*/
|
||||||
|
void onSwitcherShown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the tab switcher has been hidden.
|
||||||
|
*/
|
||||||
|
void onSwitcherHidden();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the currently selected tab has been changed.
|
||||||
|
*
|
||||||
|
* @param previousIndex
|
||||||
|
* The index of the previously selected tab as an {@link Integer} value or -1, if no
|
||||||
|
* tab was previously selected
|
||||||
|
* @param index
|
||||||
|
* The index of the currently selected tab as an {@link Integer} value or -1, if the
|
||||||
|
* tab switcher does not contain any tabs
|
||||||
|
* @param selectedTab
|
||||||
|
* The currently selected tab as an instance of the class {@link Tab} or null, if
|
||||||
|
* the tab switcher does not contain any tabs
|
||||||
|
* @param switcherHidden
|
||||||
|
* True, if selecting the tab caused the tab switcher to be hidden, false otherwise
|
||||||
|
*/
|
||||||
|
void onSelectionChanged(int previousIndex, int index, @Nullable Tab selectedTab,
|
||||||
|
boolean switcherHidden);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab has been added to the model.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which has been added, as an {@link Integer} value
|
||||||
|
* @param tab
|
||||||
|
* The tab, which has been added, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
* @param previousSelectedTabIndex
|
||||||
|
* The index of the previously selected tab as an {@link Integer} value or -1, if no
|
||||||
|
* tab was selected
|
||||||
|
* @param selectedTabIndex
|
||||||
|
* The index of the currently selected tab as an {@link Integer} value or -1, if the
|
||||||
|
* tab switcher does not contain any tabs
|
||||||
|
* @param switcherVisibilityChanged
|
||||||
|
* True, if adding the tab caused the visibility of the tab switcher to be changed,
|
||||||
|
* false otherwise
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to add the tab, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onTabAdded(int index, @NonNull Tab tab, int previousSelectedTabIndex,
|
||||||
|
int selectedTabIndex, boolean switcherVisibilityChanged,
|
||||||
|
@NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when multiple tabs have been added to the model.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index of the first tab, which has been added, as an {@link Integer} value
|
||||||
|
* @param tabs
|
||||||
|
* An array, which contains the tabs, which have been added, as an array of the type
|
||||||
|
* {@link Tab} or an empty array, if no tabs have been added
|
||||||
|
* @param previousSelectedTabIndex
|
||||||
|
* The index of the previously selected tab as an {@link Integer} value or -1, if no
|
||||||
|
* tab was selected
|
||||||
|
* @param selectedTabIndex
|
||||||
|
* The index of the currently selected tab as an {@link Integer} value or -1, if the
|
||||||
|
* tab switcher does not contain any tabs
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to add the tabs, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onAllTabsAdded(int index, @NonNull Tab[] tabs, int previousSelectedTabIndex,
|
||||||
|
int selectedTabIndex, @NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when a tab has been removed from the model.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which has been removed, as an {@link Integer} value
|
||||||
|
* @param tab
|
||||||
|
* The tab, which has been removed, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
* @param previousSelectedTabIndex
|
||||||
|
* The index of the previously selected tab as an {@link Integer} value or -1, if no
|
||||||
|
* tab was selected
|
||||||
|
* @param selectedTabIndex
|
||||||
|
* The index of the currently selected tab as an {@link Integer} value or -1, if the
|
||||||
|
* tab switcher does not contain any tabs
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to remove the tab, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onTabRemoved(int index, @NonNull Tab tab, int previousSelectedTabIndex,
|
||||||
|
int selectedTabIndex, @NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when all tabs have been removed from the tab switcher.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* An array, which contains the tabs, which have been removed, as an array of the
|
||||||
|
* type {@link Tab} or an empty array, if no tabs have been removed
|
||||||
|
* @param animation
|
||||||
|
* The animation, which has been used to remove the tabs, as an instance of the
|
||||||
|
* class {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void onAllTabsRemoved(@NonNull Tab[] tabs, @NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the padding has been changed.
|
||||||
|
*
|
||||||
|
* @param left
|
||||||
|
* The left padding, which has been set, in pixels as an {@link Integer} value
|
||||||
|
* @param top
|
||||||
|
* The top padding, which has been set, in pixels as an {@link Integer} value
|
||||||
|
* @param right
|
||||||
|
* The right padding, which has been set, in pixels as an {@link Integer} value
|
||||||
|
* @param bottom
|
||||||
|
* The bottom padding, which has been set, in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
void onPaddingChanged(int left, int top, int right, int bottom);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the default icon of a tab has been changed.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which has been set, as an instance of the class {@link Drawable} or
|
||||||
|
* null, if no icon is set
|
||||||
|
*/
|
||||||
|
void onTabIconChanged(@Nullable Drawable icon);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the background color of a tab has been changed.
|
||||||
|
*
|
||||||
|
* @param colorStateList
|
||||||
|
* The color state list, which has been set, as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if the default color should be used
|
||||||
|
*/
|
||||||
|
void onTabBackgroundColorChanged(@Nullable ColorStateList colorStateList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the text color of a tab's title has been changed.
|
||||||
|
*
|
||||||
|
* @param colorStateList
|
||||||
|
* The color state list, which has been set, as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if the default color should be used
|
||||||
|
*/
|
||||||
|
void onTabTitleColorChanged(@Nullable ColorStateList colorStateList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the icon of a tab's close button has been changed.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which has been set, as an instance of the class {@link Drawable} or
|
||||||
|
* null, if the default icon should be used
|
||||||
|
*/
|
||||||
|
void onTabCloseButtonIconChanged(@Nullable Drawable icon);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when it has been changed, whether the toolbars should be
|
||||||
|
* shown, when the tab switcher is shown, or not.
|
||||||
|
*
|
||||||
|
* @param visible
|
||||||
|
* True, if the toolbars should be shown, when the tab switcher is shown, false
|
||||||
|
* otherwise
|
||||||
|
*/
|
||||||
|
void onToolbarVisibilityChanged(boolean visible);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the title of the toolbar, which is shown, when the tab
|
||||||
|
* switcher is shown, has been changed.
|
||||||
|
*
|
||||||
|
* @param title
|
||||||
|
* The title, which has been set, as an instance of the type {@link CharSequence} or
|
||||||
|
* null, if no title is set
|
||||||
|
*/
|
||||||
|
void onToolbarTitleChanged(@Nullable CharSequence title);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the navigation icon of the toolbar, which is shown,
|
||||||
|
* when the tab switcher is shown, has been changed.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The navigation icon, which has been set, as an instance of the class {@link
|
||||||
|
* Drawable} or null, if no navigation icon is set
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be notified, when the navigation item has been
|
||||||
|
* clicked, as an instance of the type {@link OnClickListener} or null, if no
|
||||||
|
* listener should be notified
|
||||||
|
*/
|
||||||
|
void onToolbarNavigationIconChanged(@Nullable Drawable icon,
|
||||||
|
@Nullable OnClickListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method, which is invoked, when the menu of the toolbar, which is shown, when the tab
|
||||||
|
* switcher is shown, has been inflated.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the menu, which has been inflated, as an {@link Integer}
|
||||||
|
* value. The resource id must correspond to a valid menu resource
|
||||||
|
* @param listener
|
||||||
|
* The listener, which has been registered to be notified, when an item of the menu
|
||||||
|
* has been clicked, as an instance of the type OnMenuItemClickListener or null, if
|
||||||
|
* no listener should be notified
|
||||||
|
*/
|
||||||
|
void onToolbarMenuInflated(@MenuRes int resourceId,
|
||||||
|
@Nullable OnMenuItemClickListener listener);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the context, which is used by the tab switcher.
|
||||||
|
*
|
||||||
|
* @return The context, which is used by the tab switcher, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Context getContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the decorator, which allows to inflate the views, which correspond to the tabs of the
|
||||||
|
* tab switcher.
|
||||||
|
*
|
||||||
|
* @param decorator
|
||||||
|
* The decorator, which should be set, as an instance of the class {@link
|
||||||
|
* TabSwitcherDecorator}. The decorator may not be null
|
||||||
|
*/
|
||||||
|
void setDecorator(@NonNull TabSwitcherDecorator decorator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the decorator, which allows to inflate the views, which correspond to the tabs of the
|
||||||
|
* tab switcher.
|
||||||
|
*
|
||||||
|
* @return The decorator as an instance of the class {@link TabSwitcherDecorator} or null, if no
|
||||||
|
* decorator has been set
|
||||||
|
*/
|
||||||
|
TabSwitcherDecorator getDecorator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the log level, which is used for logging.
|
||||||
|
*
|
||||||
|
* @return The log level, which is used for logging, as a value of the enum LogLevel. The log
|
||||||
|
* level may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
LogLevel getLogLevel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the log level, which should be used for logging.
|
||||||
|
*
|
||||||
|
* @param logLevel
|
||||||
|
* The log level, which should be set, as a value of the enum LogLevel. The log level
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
void setLogLevel(@NonNull LogLevel logLevel);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the tab switcher is empty, or not.
|
||||||
|
*
|
||||||
|
* @return True, if the tab switcher is empty, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isEmpty();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of tabs, which are contained by the tab switcher.
|
||||||
|
*
|
||||||
|
* @return The number of tabs, which are contained by the tab switcher, as an {@link Integer}
|
||||||
|
* value
|
||||||
|
*/
|
||||||
|
int getCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab at a specific index.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index of the tab, which should be returned, as an {@link Integer} value. The
|
||||||
|
* index must be at least 0 and at maximum <code>getCount() - 1</code>, otherwise a
|
||||||
|
* {@link IndexOutOfBoundsException} will be thrown
|
||||||
|
* @return The tab, which corresponds to the given index, as an instance of the class {@link
|
||||||
|
* Tab}. The tab may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Tab getTab(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of a specific tab.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, whose index should be returned, as an instance of the class {@link Tab}. The
|
||||||
|
* tab may not be null
|
||||||
|
* @return The index of the given tab as an {@link Integer} value or -1, if the given tab is not
|
||||||
|
* contained by the tab switcher
|
||||||
|
*/
|
||||||
|
int indexOf(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new tab to the tab switcher. By default, the tab is added at the end. If the switcher
|
||||||
|
* is currently shown, the tab is added by using an animation. By default, a {@link
|
||||||
|
* SwipeAnimation} with direction {@link SwipeDirection#RIGHT} is used. If
|
||||||
|
* an animation is currently running, the tab will be added once all previously started
|
||||||
|
* animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be added, as an instance of the class {@link Tab}. The tab may
|
||||||
|
* not be null
|
||||||
|
*/
|
||||||
|
void addTab(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new tab to the tab switcher at a specific index. If the switcher is currently shown,
|
||||||
|
* the tab is added by using an animation. By default, a {@link SwipeAnimation} with
|
||||||
|
* direction {@link SwipeDirection#RIGHT} is used. If an animation is currently
|
||||||
|
* running, the tab will be added once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be added, as an instance of the class {@link Tab}. The tab may
|
||||||
|
* not be null
|
||||||
|
* @param index
|
||||||
|
* The index, the tab should be added at, as an {@link Integer} value. The index must be
|
||||||
|
* at least 0 and at maximum <code>getCount()</code>, otherwise an {@link
|
||||||
|
* IndexOutOfBoundsException} will be thrown
|
||||||
|
*/
|
||||||
|
void addTab(@NonNull Tab tab, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new tab to the tab switcher at a specific index. If the switcher is currently shown,
|
||||||
|
* the tab is added by using a specific animation. If an animation is currently
|
||||||
|
* running, the tab will be added once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be added, as an instance of the class {@link Tab}. The tab may
|
||||||
|
* not be null
|
||||||
|
* @param index
|
||||||
|
* The index, the tab should be added at, as an {@link Integer} value. The index must be
|
||||||
|
* at least 0 and at maximum <code>getCount()</code>, otherwise an {@link
|
||||||
|
* IndexOutOfBoundsException} will be thrown
|
||||||
|
* @param animation
|
||||||
|
* The animation, which should be used to add the tab, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void addTab(@NonNull Tab tab, int index, @NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all tabs, which are contained by a collection, to the tab switcher. By default, the tabs
|
||||||
|
* are added at the end. If the switcher is currently shown, the tabs are added by using an
|
||||||
|
* animation. By default, a {@link SwipeAnimation} with direction {@link
|
||||||
|
* SwipeDirection#RIGHT} is used. If an animation is currently running, the tabs will
|
||||||
|
* be added once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* A collection, which contains the tabs, which should be added, as an instance of the
|
||||||
|
* type {@link Collection} or an empty collection, if no tabs should be added
|
||||||
|
*/
|
||||||
|
void addAllTabs(@NonNull Collection<? extends Tab> tabs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all tabs, which are contained by a collection, to the tab switcher, starting at a
|
||||||
|
* specific index. If the switcher is currently shown, the tabs are added by using an animation.
|
||||||
|
* By default, a {@link SwipeAnimation} with direction {@link
|
||||||
|
* SwipeDirection#RIGHT} is used. If an animation is currently running, the tabs will
|
||||||
|
* be added once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* A collection, which contains the tabs, which should be added, as an instance of the
|
||||||
|
* type {@link Collection} or an empty collection, if no tabs should be added
|
||||||
|
* @param index
|
||||||
|
* The index, the first tab should be started at, as an {@link Integer} value. The index
|
||||||
|
* must be at least 0 and at maximum <code>getCount()</code>, otherwise an {@link
|
||||||
|
* IndexOutOfBoundsException} will be thrown
|
||||||
|
*/
|
||||||
|
void addAllTabs(@NonNull Collection<? extends Tab> tabs, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all tabs, which are contained by a collection, to the tab switcher, starting at a
|
||||||
|
* specific index. If the switcher is currently shown, the tabs are added by using a specific
|
||||||
|
* animation. If an animation is currently running, the tabs will be added once all previously
|
||||||
|
* started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* A collection, which contains the tabs, which should be added, as an instance of the
|
||||||
|
* type {@link Collection} or an empty collection, if no tabs should be added
|
||||||
|
* @param index
|
||||||
|
* The index, the first tab should be started at, as an {@link Integer} value. The index
|
||||||
|
* must be at least 0 and at maximum <code>getCount()</code>, otherwise an {@link
|
||||||
|
* IndexOutOfBoundsException} will be thrown
|
||||||
|
* @param animation
|
||||||
|
* The animation, which should be used to add the tabs, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void addAllTabs(@NonNull Collection<? extends Tab> tabs, int index,
|
||||||
|
@NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all tabs, which are contained by an array, to the tab switcher. By default, the tabs are
|
||||||
|
* added at the end. If the switcher is currently shown, the tabs are added by using an
|
||||||
|
* animation. By default, a {@link SwipeAnimation} with direction {@link
|
||||||
|
* SwipeDirection#RIGHT} is used. If an animation is currently running, the tabs will
|
||||||
|
* be added once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* An array, which contains the tabs, which should be added, as an array of the type
|
||||||
|
* {@link Tab} or an empty array, if no tabs should be added
|
||||||
|
*/
|
||||||
|
void addAllTabs(@NonNull Tab[] tabs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all tabs, which are contained by an array, to the tab switcher, starting at a specific
|
||||||
|
* index. If the switcher is currently shown, the tabs are added by using an animation. By
|
||||||
|
* default, a {@link SwipeAnimation} with direction {@link
|
||||||
|
* SwipeDirection#RIGHT} is used. If an animation is currently running, the tabs will
|
||||||
|
* be added once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* An array, which contains the tabs, which should be added, as an array of the type
|
||||||
|
* {@link Tab} or an empty array, if no tabs should be added
|
||||||
|
* @param index
|
||||||
|
* The index, the first tab should be started at, as an {@link Integer} value. The index
|
||||||
|
* must be at least 0 and at maximum <code>getCount()</code>, otherwise an {@link
|
||||||
|
* IndexOutOfBoundsException} will be thrown
|
||||||
|
*/
|
||||||
|
void addAllTabs(@NonNull Tab[] tabs, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all tabs, which are contained by an array, to the tab switcher, starting at a
|
||||||
|
* specific index. If the switcher is currently shown, the tabs are added by using a specific
|
||||||
|
* animation. If an animation is currently running, the tabs will be added once all previously
|
||||||
|
* started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tabs
|
||||||
|
* An array, which contains the tabs, which should be added, as an array of the type
|
||||||
|
* {@link Tab} or an empty array, if no tabs should be added
|
||||||
|
* @param index
|
||||||
|
* The index, the first tab should be started at, as an {@link Integer} value. The index
|
||||||
|
* must be at least 0 and at maximum <code>getCount()</code>, otherwise an {@link
|
||||||
|
* IndexOutOfBoundsException} will be thrown
|
||||||
|
* @param animation
|
||||||
|
* The animation, which should be used to add the tabs, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void addAllTabs(@NonNull Tab[] tabs, int index, @NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a specific tab from the tab switcher. If the switcher is currently shown, the tab is
|
||||||
|
* removed by using an animation. By default, a {@link SwipeAnimation} with direction
|
||||||
|
* {@link SwipeDirection#RIGHT} is used. If an animation is currently running, the tab
|
||||||
|
* will be removed once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be removed, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
*/
|
||||||
|
void removeTab(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a specific tab from the tab switcher. If the switcher is currently shown, the tab is
|
||||||
|
* removed by using a specific animation. If an animation is currently running, the
|
||||||
|
* tab will be removed once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be removed, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null
|
||||||
|
* @param animation
|
||||||
|
* The animation, which should be used to remove the tabs, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void removeTab(@NonNull Tab tab, @NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all tabs from the tab switcher. If the switcher is currently shown, the tabs are
|
||||||
|
* removed by using an animation. By default, a {@link SwipeAnimation} with direction
|
||||||
|
* {@link SwipeDirection#RIGHT} is used. If an animation is currently running, the
|
||||||
|
* tabs will be removed once all previously started animations have been finished.
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all tabs from the tab switcher. If the switcher is currently shown, the tabs are
|
||||||
|
* removed by using a specific animation. If an animation is currently running, the
|
||||||
|
* tabs will be removed once all previously started animations have been finished.
|
||||||
|
*
|
||||||
|
* @param animation
|
||||||
|
* The animation, which should be used to remove the tabs, as an instance of the class
|
||||||
|
* {@link Animation}. The animation may not be null
|
||||||
|
*/
|
||||||
|
void clear(@NonNull Animation animation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the tab switcher is currently shown.
|
||||||
|
*
|
||||||
|
* @return True, if the tab switcher is currently shown, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isSwitcherShown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the tab switcher by using an animation, if it is not already shown.
|
||||||
|
*/
|
||||||
|
void showSwitcher();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the tab switcher by using an animation, if it is currently shown.
|
||||||
|
*/
|
||||||
|
void hideSwitcher();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the tab switcher by using an animation, i.e. if the switcher is
|
||||||
|
* currently shown, it is hidden, otherwise it is shown.
|
||||||
|
*/
|
||||||
|
void toggleSwitcherVisibility();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected tab.
|
||||||
|
*
|
||||||
|
* @return The currently selected tab as an instance of the class {@link Tab} or null, if no tab
|
||||||
|
* is currently selected
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
Tab getSelectedTab();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the currently selected tab.
|
||||||
|
*
|
||||||
|
* @return The index of the currently selected tab as an {@link Integer} value or -1, if no tab
|
||||||
|
* is currently selected
|
||||||
|
*/
|
||||||
|
int getSelectedTabIndex();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a specific tab.
|
||||||
|
*
|
||||||
|
* @param tab
|
||||||
|
* The tab, which should be selected, as an instance of the class {@link Tab}. The tab
|
||||||
|
* may not be null. If the tab is not contained by the tab switcher, a {@link
|
||||||
|
* NoSuchElementException} will be thrown
|
||||||
|
*/
|
||||||
|
void selectTab(@NonNull Tab tab);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the padding of the tab switcher.
|
||||||
|
*
|
||||||
|
* @param left
|
||||||
|
* The left padding, which should be set, in pixels as an {@link Integer} value
|
||||||
|
* @param top
|
||||||
|
* The top padding, which should be set, in pixels as an {@link Integer} value
|
||||||
|
* @param right
|
||||||
|
* The right padding, which should be set, in pixels as an {@link Integer} value
|
||||||
|
* @param bottom
|
||||||
|
* The bottom padding, which should be set, in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
void setPadding(int left, int top, int right, int bottom);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the left padding of the tab switcher.
|
||||||
|
*
|
||||||
|
* @return The left padding of the tab switcher in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPaddingLeft();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the top padding of the tab switcher.
|
||||||
|
*
|
||||||
|
* @return The top padding of the tab switcher in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPaddingTop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the right padding of the tab switcher.
|
||||||
|
*
|
||||||
|
* @return The right padding of the tab switcher in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPaddingRight();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the bottom padding of the tab switcher.
|
||||||
|
*
|
||||||
|
* @return The bottom padding of the tab switcher in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPaddingBottom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the start padding of the tab switcher. This corresponds to the right padding, if a
|
||||||
|
* right-to-left layout is used, or to the left padding otherwise.
|
||||||
|
*
|
||||||
|
* @return The start padding of the tab switcher in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPaddingStart();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the end padding of the tab switcher. This corresponds ot the left padding, if a
|
||||||
|
* right-to-left layout is used, or to the right padding otherwise.
|
||||||
|
*
|
||||||
|
* @return The end padding of the tab switcher in pixels as an {@link Integer} value
|
||||||
|
*/
|
||||||
|
int getPaddingEnd();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default icon of a tab.
|
||||||
|
*
|
||||||
|
* @return The default icon of a tab as an instance of the class {@link Drawable} or null, if no
|
||||||
|
* icon is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
Drawable getTabIcon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default icon of a tab.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the icon, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid drawable resource
|
||||||
|
*/
|
||||||
|
void setTabIcon(@DrawableRes int resourceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default icon of a tab.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which should be set, as an instance of the class {@link Bitmap} or null, if
|
||||||
|
* no icon should be set
|
||||||
|
*/
|
||||||
|
void setTabIcon(@Nullable Bitmap icon);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default background color of a tab.
|
||||||
|
*
|
||||||
|
* @return The default background color of a tab as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if the default color is used
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
ColorStateList getTabBackgroundColor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default background color of a tab.
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* The color, which should be set, as an {@link Integer} value or -1, if the default
|
||||||
|
* color should be used
|
||||||
|
*/
|
||||||
|
void setTabBackgroundColor(@ColorInt int color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default background color of a tab.
|
||||||
|
*
|
||||||
|
* @param colorStateList
|
||||||
|
* The color, which should be set, as an instance of the class {@link ColorStateList} or
|
||||||
|
* null, if the default color should be used
|
||||||
|
*/
|
||||||
|
void setTabBackgroundColor(@Nullable ColorStateList colorStateList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default text color of a tab's title.
|
||||||
|
*
|
||||||
|
* @return The default text color of a tab's title as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if the default color is used
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
ColorStateList getTabTitleTextColor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default text color of a tab's title.
|
||||||
|
*
|
||||||
|
* @param color
|
||||||
|
* The color, which should be set, as an {@link Integer} value or -1, if the default
|
||||||
|
* color should be used
|
||||||
|
*/
|
||||||
|
void setTabTitleTextColor(@ColorInt int color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default text color of a tab's title.
|
||||||
|
*
|
||||||
|
* @param colorStateList
|
||||||
|
* The color state list, which should be set, as an instance of the class {@link
|
||||||
|
* ColorStateList} or null, if the default color should be used
|
||||||
|
*/
|
||||||
|
void setTabTitleTextColor(@Nullable ColorStateList colorStateList);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default icon of a tab's close button.
|
||||||
|
*
|
||||||
|
* @return The default icon of a tab's close button as an instance of the class {@link Drawable}
|
||||||
|
* or null, if the default icon is used
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
Drawable getTabCloseButtonIcon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default icon of a tab's close button.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the icon, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid drawable resource
|
||||||
|
*/
|
||||||
|
void setTabCloseButtonIcon(@DrawableRes int resourceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default icon of a tab's close button.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which should be set, as an instance of the class {@link Bitmap} or null, if
|
||||||
|
* the default icon should be used
|
||||||
|
*/
|
||||||
|
void setTabCloseButtonIcon(@Nullable final Bitmap icon);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the toolbars are shown, when the tab switcher is shown, or not. When using
|
||||||
|
* the tablet layout, the toolbars are always shown.
|
||||||
|
*
|
||||||
|
* @return True, if the toolbars are shown, false otherwise
|
||||||
|
*/
|
||||||
|
boolean areToolbarsShown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets, whether the toolbars should be shown, when the tab switcher is shown, or not. This
|
||||||
|
* method does not have any effect when using the tablet layout.
|
||||||
|
*
|
||||||
|
* @param show
|
||||||
|
* True, if the toolbars should be shown, false otherwise
|
||||||
|
*/
|
||||||
|
void showToolbars(boolean show);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the title of the toolbar, which is shown, when the tab switcher is shown. When using
|
||||||
|
* the tablet layout, the title corresponds to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @return The title of the toolbar, which is shown, when the tab switcher is shown, as an
|
||||||
|
* instance of the type {@link CharSequence} or null, if no title is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
CharSequence getToolbarTitle();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the title of the toolbar, which is shown, when the tab switcher is shown. When using the
|
||||||
|
* tablet layout, the title is set to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the title, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid string resource
|
||||||
|
*/
|
||||||
|
void setToolbarTitle(@StringRes int resourceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the title of the toolbar, which is shown, when the tab switcher is shown. When using the
|
||||||
|
* tablet layout, the title is set to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @param title
|
||||||
|
* The title, which should be set, as an instance of the type {@link CharSequence} or
|
||||||
|
* null, if no title should be set
|
||||||
|
*/
|
||||||
|
void setToolbarTitle(@Nullable CharSequence title);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the navigation icon of the toolbar, which is shown, when the tab switcher is shown.
|
||||||
|
* When using the tablet layout, the icon corresponds to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @return The icon of the toolbar, which is shown, when the tab switcher is shown, as an
|
||||||
|
* instance of the class {@link Drawable} or null, if no icon is set
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
Drawable getToolbarNavigationIcon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the navigation icon of the toolbar, which is shown, when the tab switcher is shown. When
|
||||||
|
* using the tablet layout, the icon is set to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the icon, which should be set, as an {@link Integer} value. The
|
||||||
|
* resource id must correspond to a valid drawable resource
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be notified, when the navigation item has been clicked, as
|
||||||
|
* an instance of the type {@link OnClickListener} or null, if no listener should be
|
||||||
|
* notified
|
||||||
|
*/
|
||||||
|
void setToolbarNavigationIcon(@DrawableRes int resourceId, @Nullable OnClickListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the navigation icon of the toolbar, which is shown, when the tab switcher is shown. When
|
||||||
|
* using the tablet layout, the icon is set to the primary toolbar.
|
||||||
|
*
|
||||||
|
* @param icon
|
||||||
|
* The icon, which should be set, as an instance of the class {@link Drawable} or null,
|
||||||
|
* if no icon should be set
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be notified, when the navigation item has been clicked, as
|
||||||
|
* an instance of the type {@link OnClickListener} or null, if no listener should be
|
||||||
|
* notified
|
||||||
|
*/
|
||||||
|
void setToolbarNavigationIcon(@Nullable Drawable icon, @Nullable OnClickListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inflates the menu of the toolbar, which is shown, when the tab switcher is shown. When using
|
||||||
|
* the tablet layout, the menu is inflated into the secondary toolbar.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The resource id of the menu, which should be inflated, as an {@link Integer} value.
|
||||||
|
* The resource id must correspond to a valid menu resource
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be notified, when an menu item has been clicked, as an
|
||||||
|
* instance of the type OnMenuItemClickListener or null, if no listener should be
|
||||||
|
* notified
|
||||||
|
*/
|
||||||
|
void inflateToolbarMenu(@MenuRes int resourceId, @Nullable OnMenuItemClickListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new listener, which should be notified, when a tab is about to be closed by clicking
|
||||||
|
* its close button.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be added, as an instance of the type {@link
|
||||||
|
* TabCloseListener}. The listener may not be null
|
||||||
|
*/
|
||||||
|
void addCloseTabListener(@NonNull TabCloseListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a specific listener, which should not be notified, when a tab is about to be closed
|
||||||
|
* by clicking its close button, anymore.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be removed, as an instance of the type {@link
|
||||||
|
* TabCloseListener}. The listener may not be null
|
||||||
|
*/
|
||||||
|
void removeCloseTabListener(@NonNull TabCloseListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new listener, which should be notified, when the preview of a tab is about to be
|
||||||
|
* loaded. Previews are only loaded when using the smartphone layout.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be added, as an instance of the type {@link
|
||||||
|
* TabPreviewListener}. The listener may not be null
|
||||||
|
*/
|
||||||
|
void addTabPreviewListener(@NonNull TabPreviewListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a specific listener, which should not be notified, when the preview of a tab is about
|
||||||
|
* to be loaded.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* The listener, which should be removed, as an instance of the type {@link
|
||||||
|
* TabPreviewListener}. The listener may not be null
|
||||||
|
*/
|
||||||
|
void removeTabPreviewListener(@NonNull TabPreviewListener listener);
|
||||||
|
|
||||||
|
}
|
46
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/model/Restorable.java
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.model;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the interface, a class, whose state should be stored and restored, must implement.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public interface Restorable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the current state.
|
||||||
|
*
|
||||||
|
* @param outState
|
||||||
|
* The bundle, which should be used to store the saved state, as an instance of the
|
||||||
|
* class {@link Bundle}. The bundle may not be null
|
||||||
|
*/
|
||||||
|
void saveInstanceState(@NonNull Bundle outState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a previously saved state.
|
||||||
|
*
|
||||||
|
* @param savedInstanceState
|
||||||
|
* The saved state as an instance of the class {@link Bundle} or null, if no saved state
|
||||||
|
* is available
|
||||||
|
*/
|
||||||
|
void restoreInstanceState(@Nullable Bundle savedInstanceState);
|
||||||
|
|
||||||
|
}
|
49
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/model/State.java
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all possible states of a tab, while the switcher is shown.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public enum State {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab is part of the stack, which is located at the start of the switcher.
|
||||||
|
*/
|
||||||
|
STACKED_START,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab is displayed atop of the stack, which is located at the start of the switcher.
|
||||||
|
*/
|
||||||
|
STACKED_START_ATOP,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab is floating and freely movable.
|
||||||
|
*/
|
||||||
|
FLOATING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab is part of the stack, which is located at the end of the switcher.
|
||||||
|
*/
|
||||||
|
STACKED_END,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the tab is currently not visible, i.e. if no view is inflated to visualize it.
|
||||||
|
*/
|
||||||
|
HIDDEN
|
||||||
|
|
||||||
|
}
|
305
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/model/TabItem.java
Executable file
@ -0,0 +1,305 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.model;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.layout.phone.PhoneTabViewHolder;
|
||||||
|
import de.mrapp.android.util.view.AttachedViewRecycler;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureAtLeast;
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An item, which contains information about a tab of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class TabItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A comparator, which allows to compare two instances of the class {@link TabItem}.
|
||||||
|
*/
|
||||||
|
public static class Comparator implements java.util.Comparator<TabItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab switcher, the tab items, which are compared by the comparator, belong to.
|
||||||
|
*/
|
||||||
|
private final TabSwitcher tabSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new comparator, which allows to compare two instances of the class {@link
|
||||||
|
* TabItem}.
|
||||||
|
*
|
||||||
|
* @param tabSwitcher
|
||||||
|
* The tab switcher, the tab items, which should be compared by the comparator,
|
||||||
|
* belong to, as a instance of the class {@link TabSwitcher}. The tab switcher may
|
||||||
|
* not be null
|
||||||
|
*/
|
||||||
|
public Comparator(@NonNull final TabSwitcher tabSwitcher) {
|
||||||
|
ensureNotNull(tabSwitcher, "The tab switcher may not be null");
|
||||||
|
this.tabSwitcher = tabSwitcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(final TabItem o1, final TabItem o2) {
|
||||||
|
Tab tab1 = o1.getTab();
|
||||||
|
Tab tab2 = o2.getTab();
|
||||||
|
int index1 = tabSwitcher.indexOf(tab1);
|
||||||
|
int index2 = tabSwitcher.indexOf(tab2);
|
||||||
|
|
||||||
|
if (index2 == -1) {
|
||||||
|
index2 = o2.getIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index1 == -1 || index2 == -1) {
|
||||||
|
throw new RuntimeException("Tab not contained by tab switcher");
|
||||||
|
}
|
||||||
|
|
||||||
|
return index1 < index2 ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the tab.
|
||||||
|
*/
|
||||||
|
private final int index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab.
|
||||||
|
*/
|
||||||
|
private final Tab tab;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view, which is used to visualize the tab.
|
||||||
|
*/
|
||||||
|
private View view;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view holder, which stores references the views, which belong to the tab.
|
||||||
|
*/
|
||||||
|
private PhoneTabViewHolder viewHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tag, which is associated with the tab.
|
||||||
|
*/
|
||||||
|
private Tag tag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new item, which contains information about a tab of a {@link TabSwitcher}. By
|
||||||
|
* default, the item is neither associated with a view, nor with a view holder.
|
||||||
|
*
|
||||||
|
* @param index
|
||||||
|
* The index of the tab as an {@link Integer} value. The index must be at least 0
|
||||||
|
* @param tab
|
||||||
|
* The tab as an instance of the class {@link Tab}. The tab may not be null
|
||||||
|
*/
|
||||||
|
public TabItem(final int index, @NonNull final Tab tab) {
|
||||||
|
ensureAtLeast(index, 0, "The index must be at least 0");
|
||||||
|
ensureNotNull(tab, "The tab may not be null");
|
||||||
|
this.index = index;
|
||||||
|
this.tab = tab;
|
||||||
|
this.view = null;
|
||||||
|
this.viewHolder = null;
|
||||||
|
this.tag = new Tag();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new item, which contains information about a tab of a tab switcher. By
|
||||||
|
* default, the item is neither associated with a view, nor with a view holder.
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
* The model, the tab belongs to, as an instance of the type {@link Model}. The model
|
||||||
|
* may not be null
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which is used to reuse the views, which are used to visualize
|
||||||
|
* tabs, as an instance of the class AttachedViewRecycler. The view recycler may not be
|
||||||
|
* null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab as an {@link Integer} value. The index must be at least 0
|
||||||
|
* @return The item, which has been created, as an instance of the class {@link TabItem}. The
|
||||||
|
* item may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static TabItem create(@NonNull final Model model,
|
||||||
|
@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler,
|
||||||
|
final int index) {
|
||||||
|
Tab tab = model.getTab(index);
|
||||||
|
return create(viewRecycler, index, tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new item, which contains information about a specific tab. By default, the item is
|
||||||
|
* neither associated with a view, nor with a view holder.
|
||||||
|
*
|
||||||
|
* @param viewRecycler
|
||||||
|
* The view recycler, which is used to reuse the views, which are used to visualize
|
||||||
|
* tabs, as an instance of the class AttachedViewRecycler. The view recycler may not be
|
||||||
|
* null
|
||||||
|
* @param index
|
||||||
|
* The index of the tab as an {@link Integer} value. The index must be at least 0
|
||||||
|
* @param tab
|
||||||
|
* The tab as an instance of the class {@link Tab}. The tab may not be null
|
||||||
|
* @return The item, which has been created, as an instance of the class {@link TabItem}. The
|
||||||
|
* item may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static TabItem create(@NonNull final AttachedViewRecycler<TabItem, ?> viewRecycler,
|
||||||
|
final int index, @NonNull final Tab tab) {
|
||||||
|
TabItem tabItem = new TabItem(index, tab);
|
||||||
|
View view = viewRecycler.getView(tabItem);
|
||||||
|
|
||||||
|
if (view != null) {
|
||||||
|
tabItem.setView(view);
|
||||||
|
tabItem.setViewHolder((PhoneTabViewHolder) view.getTag(R.id.tag_view_holder));
|
||||||
|
Tag tag = (Tag) view.getTag(R.id.tag_properties);
|
||||||
|
|
||||||
|
if (tag != null) {
|
||||||
|
tabItem.setTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the tab.
|
||||||
|
*
|
||||||
|
* @return The index of the tab as an {@link Integer} value. The index must be at least 0
|
||||||
|
*/
|
||||||
|
public final int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tab.
|
||||||
|
*
|
||||||
|
* @return The tab as an instance of the class {@link Tab}. The tab may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Tab getTab() {
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view, which is used to visualize the tab.
|
||||||
|
*
|
||||||
|
* @return The view, which is used to visualize the tab, as an instance of the class {@link
|
||||||
|
* View} or null, if no such view is currently inflated
|
||||||
|
*/
|
||||||
|
public final View getView() {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the view, which is used to visualize the tab.
|
||||||
|
*
|
||||||
|
* @param view
|
||||||
|
* The view, which should be set, as an instance of the class {@link View} or null, if
|
||||||
|
* no view should be set
|
||||||
|
*/
|
||||||
|
public final void setView(@Nullable final View view) {
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view holder, which stores references to the views, which belong to the tab.
|
||||||
|
*
|
||||||
|
* @return The view holder as an instance of the class {@link PhoneTabViewHolder} or null, if no
|
||||||
|
* view is is currently inflated to visualize the tab
|
||||||
|
*/
|
||||||
|
public final PhoneTabViewHolder getViewHolder() {
|
||||||
|
return viewHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the view holder, which stores references to the views, which belong to the tab.
|
||||||
|
*
|
||||||
|
* @param viewHolder
|
||||||
|
* The view holder, which should be set, as an instance of the class {@link
|
||||||
|
* PhoneTabViewHolder} or null, if no view holder should be set
|
||||||
|
*/
|
||||||
|
public final void setViewHolder(@Nullable final PhoneTabViewHolder viewHolder) {
|
||||||
|
this.viewHolder = viewHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag, which is associated with the tab.
|
||||||
|
*
|
||||||
|
* @return The tag as an instance of the class {@link Tag}. The tag may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Tag getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tag, which is associated with the tab.
|
||||||
|
*
|
||||||
|
* @param tag
|
||||||
|
* The tag, which should be set, as an instance of the class {@link Tag}. The tag may
|
||||||
|
* not be null
|
||||||
|
*/
|
||||||
|
public final void setTag(@NonNull final Tag tag) {
|
||||||
|
ensureNotNull(tag, "The tag may not be null");
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether a view, which is used to visualize the tab, is currently inflated, or not.
|
||||||
|
*
|
||||||
|
* @return True, if a view, which is used to visualize the tab, is currently inflated, false
|
||||||
|
* otherwise
|
||||||
|
*/
|
||||||
|
public final boolean isInflated() {
|
||||||
|
return view != null && viewHolder != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the tab is currently visible, or not.
|
||||||
|
*
|
||||||
|
* @return True, if the tab is currently visible, false otherwise
|
||||||
|
*/
|
||||||
|
public final boolean isVisible() {
|
||||||
|
return tag.getState() != State.HIDDEN || tag.isClosing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString() {
|
||||||
|
return "TabItem [index = " + index + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int hashCode() {
|
||||||
|
return tab.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean equals(final Object obj) {
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (obj.getClass() != getClass())
|
||||||
|
return false;
|
||||||
|
TabItem other = (TabItem) obj;
|
||||||
|
return tab.equals(other.tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1298
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/model/TabSwitcherModel.java
Executable file
131
chrome-tabs/src/main/java/de/mrapp/android/tabswitcher/model/Tag.java
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.model;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
|
||||||
|
import static de.mrapp.android.util.Condition.ensureNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tag, which allows to store the properties of the tabs of a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class Tag implements Cloneable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of the tab on the dragging axis.
|
||||||
|
*/
|
||||||
|
private float position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the tab.
|
||||||
|
*/
|
||||||
|
private State state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True, if the tab is currently being closed, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean closing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new tag, which allows to store the properties of the tabs of a {@link
|
||||||
|
* TabSwitcher}.
|
||||||
|
*/
|
||||||
|
public Tag() {
|
||||||
|
setPosition(Float.NaN);
|
||||||
|
setState(State.HIDDEN);
|
||||||
|
setClosing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the position of the tab on the dragging axis.
|
||||||
|
*
|
||||||
|
* @return The position of the tab as a {@link Float} value
|
||||||
|
*/
|
||||||
|
public final float getPosition() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position of the tab on the dragging axis.
|
||||||
|
*
|
||||||
|
* @param position
|
||||||
|
* The position, which should be set, as a {@link Float} value
|
||||||
|
*/
|
||||||
|
public final void setPosition(final float position) {
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the state of the tab.
|
||||||
|
*
|
||||||
|
* @return The state of the tab as a value of the enum {@link State}. The state may not be null
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final State getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the state of the tab.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* The state, which should be set, as a value of the enum {@link State}. The state may
|
||||||
|
* not be null
|
||||||
|
*/
|
||||||
|
public final void setState(@NonNull final State state) {
|
||||||
|
ensureNotNull(state, "The state may not be null");
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns, whether the tab is currently being closed, or not.
|
||||||
|
*
|
||||||
|
* @return True, if the tab is currently being closed, false otherwise
|
||||||
|
*/
|
||||||
|
public final boolean isClosing() {
|
||||||
|
return closing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets, whether the tab is currently being closed, or not.
|
||||||
|
*
|
||||||
|
* @param closing
|
||||||
|
* True, if the tab is currently being closed, false otherwise
|
||||||
|
*/
|
||||||
|
public final void setClosing(final boolean closing) {
|
||||||
|
this.closing = closing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final Tag clone() {
|
||||||
|
Tag clone;
|
||||||
|
|
||||||
|
try {
|
||||||
|
clone = (Tag) super.clone();
|
||||||
|
} catch (ClassCastException | CloneNotSupportedException e) {
|
||||||
|
clone = new Tag();
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.position = position;
|
||||||
|
clone.state = state;
|
||||||
|
clone.closing = closing;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 - 2017 Michael Rapp
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||||
|
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||||
|
* or implied. See the License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package de.mrapp.android.tabswitcher.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.annotation.AttrRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.AppCompatImageButton;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import de.mrapp.android.tabswitcher.Animation;
|
||||||
|
import de.mrapp.android.tabswitcher.R;
|
||||||
|
import de.mrapp.android.tabswitcher.Tab;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcher;
|
||||||
|
import de.mrapp.android.tabswitcher.TabSwitcherListener;
|
||||||
|
import de.mrapp.android.tabswitcher.drawable.TabSwitcherDrawable;
|
||||||
|
import de.mrapp.android.util.ThemeUtil;
|
||||||
|
import de.mrapp.android.util.ViewUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An image button, which allows to display the number of tabs, which are currently contained by a
|
||||||
|
* {@link TabSwitcher} by using a {@link TabSwitcherDrawable}. It must be registered at a {@link
|
||||||
|
* TabSwitcher} instance in order to keep the displayed count up to date. It therefore implements
|
||||||
|
* the interface {@link TabSwitcherListener}.
|
||||||
|
*
|
||||||
|
* @author Michael Rapp
|
||||||
|
* @since 0.1.0
|
||||||
|
*/
|
||||||
|
public class TabSwitcherButton extends AppCompatImageButton implements TabSwitcherListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drawable, which is used by the image button.
|
||||||
|
*/
|
||||||
|
private TabSwitcherDrawable drawable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the view.
|
||||||
|
*/
|
||||||
|
private void initialize() {
|
||||||
|
drawable = new TabSwitcherDrawable(getContext());
|
||||||
|
setImageDrawable(drawable);
|
||||||
|
ViewUtil.setBackground(this,
|
||||||
|
ThemeUtil.getDrawable(getContext(), R.attr.selectableItemBackgroundBorderless));
|
||||||
|
setContentDescription(null);
|
||||||
|
setClickable(true);
|
||||||
|
setFocusable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new image button, which allows to display the number of tabs, which are currently
|
||||||
|
* contained by a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used by the view, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
*/
|
||||||
|
public TabSwitcherButton(@NonNull final Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new image button, which allows to display the number of tabs, which are currently
|
||||||
|
* contained by a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used by the view, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
* @param attributeSet
|
||||||
|
* The attribute set, the view's attributes should be obtained from, as an instance of
|
||||||
|
* the type {@link AttributeSet} or null, if no attributes should be obtained
|
||||||
|
*/
|
||||||
|
public TabSwitcherButton(@NonNull final Context context,
|
||||||
|
@Nullable final AttributeSet attributeSet) {
|
||||||
|
super(context, attributeSet);
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new image button, which allows to display the number of tabs, which are currently
|
||||||
|
* contained by a {@link TabSwitcher}.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* The context, which should be used by the view, as an instance of the class {@link
|
||||||
|
* Context}. The context may not be null
|
||||||
|
* @param attributeSet
|
||||||
|
* The attribute set, the view's attributes should be obtained from, as an instance of
|
||||||
|
* the type {@link AttributeSet} or null, if no attributes should be obtained
|
||||||
|
* @param defaultStyle
|
||||||
|
* The default style to apply to this view. If 0, no style will be applied (beyond what
|
||||||
|
* is included in the theme). This may either be an attribute resource, whose value will
|
||||||
|
* be retrieved from the current theme, or an explicit style resource
|
||||||
|
*/
|
||||||
|
public TabSwitcherButton(@NonNull final Context context,
|
||||||
|
@Nullable final AttributeSet attributeSet,
|
||||||
|
@AttrRes final int defaultStyle) {
|
||||||
|
super(context, attributeSet, defaultStyle);
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the image button to display a specific value.
|
||||||
|
*
|
||||||
|
* @param count
|
||||||
|
* The value, which should be displayed, as an {@link Integer} value. The value must be
|
||||||
|
* at least 0
|
||||||
|
*/
|
||||||
|
public final void setCount(final int count) {
|
||||||
|
drawable.setCount(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSwitcherShown(@NonNull final TabSwitcher tabSwitcher) {
|
||||||
|
drawable.onSwitcherShown(tabSwitcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSwitcherHidden(@NonNull final TabSwitcher tabSwitcher) {
|
||||||
|
drawable.onSwitcherHidden(tabSwitcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onSelectionChanged(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
final int selectedTabIndex,
|
||||||
|
@Nullable final Tab selectedTab) {
|
||||||
|
drawable.onSelectionChanged(tabSwitcher, selectedTabIndex, selectedTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabAdded(@NonNull final TabSwitcher tabSwitcher, final int index,
|
||||||
|
@NonNull final Tab tab, @NonNull final Animation animation) {
|
||||||
|
drawable.onTabAdded(tabSwitcher, index, tab, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onTabRemoved(@NonNull final TabSwitcher tabSwitcher, final int index,
|
||||||
|
@NonNull final Tab tab, @NonNull final Animation animation) {
|
||||||
|
drawable.onTabRemoved(tabSwitcher, index, tab, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onAllTabsRemoved(@NonNull final TabSwitcher tabSwitcher,
|
||||||
|
@NonNull final Tab[] tabs,
|
||||||
|
@NonNull final Animation animation) {
|
||||||
|
drawable.onAllTabsRemoved(tabSwitcher, tabs, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
chrome-tabs/src/main/res/drawable-hdpi/phone_close_tab_icon.png
Executable file
After Width: | Height: | Size: 395 B |
BIN
chrome-tabs/src/main/res/drawable-hdpi/phone_tab_background.9.png
Executable file
After Width: | Height: | Size: 2.4 KiB |
BIN
chrome-tabs/src/main/res/drawable-hdpi/phone_tab_border.9.png
Executable file
After Width: | Height: | Size: 465 B |
BIN
chrome-tabs/src/main/res/drawable-hdpi/tab_switcher_drawable_background.png
Executable file
After Width: | Height: | Size: 250 B |
BIN
chrome-tabs/src/main/res/drawable-mdpi/ic_close_tab_18dp.png
Executable file
After Width: | Height: | Size: 292 B |
BIN
chrome-tabs/src/main/res/drawable-mdpi/phone_tab_background.9.png
Executable file
After Width: | Height: | Size: 1.0 KiB |
BIN
chrome-tabs/src/main/res/drawable-mdpi/phone_tab_border.9.png
Executable file
After Width: | Height: | Size: 315 B |