Feature: Terminal session and extra keys

This commit is contained in:
zt515 2017-06-11 19:24:09 +08:00
parent 9407edb905
commit acff62db44
130 changed files with 21509 additions and 0 deletions

10
.gitignore vendored
View File

@ -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
View File

@ -0,0 +1 @@
/build

7
app/CMakeLists.txt Normal file
View 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
View 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
View 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

View File

@ -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)
}
}

View 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
View 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);
}

View 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)
}
}

View 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;
}
}

View 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";
}

View 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);
}

View 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;
}
}

View 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);
}
}
}
}

View 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;
}
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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();
}

View 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];
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

View 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" />

View 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" />

View 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>

View 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"/>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View 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>

View 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>

View File

@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View 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
View 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
View File

@ -0,0 +1 @@
/build

25
chrome-tabs/build.gradle Executable file
View 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
View File

@ -0,0 +1,3 @@
POM_NAME=ChromeLikeTabSwitcher
POM_ARTIFACT_ID=chrome-like-tab-switcher
POM_PACKAGING=aar

View 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"/>

View 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;
}
}

View 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
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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);
}

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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);
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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]);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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) {
}
}

View 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.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;
}

View 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);
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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;
}

View File

@ -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());
}
}

View 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);
}

View 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);
}

View 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
}

View 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);
}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View File

@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Some files were not shown because too many files have changed in this diff Show More