Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support RTL #47

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 64 additions & 36 deletions FloatRatingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import UIKit
@objc public protocol FloatRatingViewDelegate {
/// Returns the rating value when touch events end
@objc optional func floatRatingView(_ ratingView: FloatRatingView, didUpdate rating: Double)

/// Returns the rating value as the user pans
@objc optional func floatRatingView(_ ratingView: FloatRatingView, isUpdating rating: Double)
}
Expand All @@ -24,13 +24,13 @@ open class FloatRatingView: UIView {
// MARK: Properties

open weak var delegate: FloatRatingViewDelegate?

/// Array of empty image views
private var emptyImageViews: [UIImageView] = []

/// Array of full image views
private var fullImageViews: [UIImageView] = []

/// Sets the empty image (e.g. a star outline)
@IBInspectable open var emptyImage: UIImage? {
didSet {
Expand All @@ -41,7 +41,7 @@ open class FloatRatingView: UIView {
refresh()
}
}

/// Sets the full image that is overlayed on top of the empty image.
/// Should be same size and shape as the empty image.
@IBInspectable open var fullImage: UIImage? {
Expand All @@ -53,10 +53,10 @@ open class FloatRatingView: UIView {
refresh()
}
}

/// Sets the empty and full image view content mode.
open var imageContentMode: UIView.ContentMode = .scaleAspectFit

/// Minimum rating.
@IBInspectable open var minRating: Int = 0 {
didSet {
Expand All @@ -67,7 +67,7 @@ open class FloatRatingView: UIView {
}
}
}

/// Max rating value.
@IBInspectable open var maxRating: Int = 5 {
didSet {
Expand All @@ -81,10 +81,10 @@ open class FloatRatingView: UIView {
}
}
}

/// Minimum image size.
@IBInspectable open var minImageSize = CGSize(width: 5.0, height: 5.0)

/// Set the current rating.
@IBInspectable open var rating: Double = 0 {
didSet {
Expand All @@ -93,26 +93,26 @@ open class FloatRatingView: UIView {
}
}
}

/// Sets whether or not the rating view can be changed by panning.
@IBInspectable open var editable = true

// MARK: Type

@objc public enum FloatRatingViewType: Int {
/// Integer rating
case wholeRatings
/// Double rating in increments of 0.5
case halfRatings
/// Double rating
case floatRatings

/// Returns true if rating can contain decimal places
func supportsFractions() -> Bool {
return self == .halfRatings || self == .floatRatings
}
}

/// Float rating view type
@IBInspectable open var type: FloatRatingViewType = .wholeRatings

Expand All @@ -131,28 +131,28 @@ open class FloatRatingView: UIView {
}

// MARK: Helper methods

private func initImageViews() {
guard emptyImageViews.isEmpty && fullImageViews.isEmpty else {
return
}

// Add new image views
for _ in 0..<maxRating {
let emptyImageView = UIImageView()
emptyImageView.contentMode = imageContentMode
emptyImageView.image = emptyImage
emptyImageViews.append(emptyImageView)
addSubview(emptyImageView)

let fullImageView = UIImageView()
fullImageView.contentMode = imageContentMode
fullImageView.image = fullImage
fullImageViews.append(fullImageView)
addSubview(fullImageView)
}
}

private func removeImageViews() {
// Remove old image views
for i in 0..<emptyImageViews.count {
Expand All @@ -164,12 +164,12 @@ open class FloatRatingView: UIView {
emptyImageViews.removeAll(keepingCapacity: false)
fullImageViews.removeAll(keepingCapacity: false)
}

// Refresh hides or shows full images
private func refresh() {
for i in 0..<fullImageViews.count {
let imageView = fullImageViews[i]

if rating >= Double(i+1) {
imageView.layer.mask = nil
imageView.isHidden = false
Expand All @@ -185,6 +185,8 @@ open class FloatRatingView: UIView {
imageView.isHidden = true
}
}

flipViewIfNeeded()
}

// Calculates the ideal ImageView size in a given CGSize
Expand All @@ -204,24 +206,24 @@ open class FloatRatingView: UIView {
return CGSize(width: size.width, height: height)
}
}

// Calculates new rating based on touch location in view
private func updateLocation(_ touch: UITouch) {
guard editable else {
return
}

let touchLocation = touch.location(in: self)
var newRating: Double = 0
for i in stride(from: (maxRating-1), through: 0, by: -1) {
let imageView = emptyImageViews[i]
guard touchLocation.x > imageView.frame.origin.x else {
continue
}

// Find touch point in image view
let newLocation = imageView.convert(touchLocation, from: self)

// Find decimal value for float or half rating
if imageView.point(inside: newLocation, with: nil) && (type.supportsFractions()) {
let decimalNum = Double(newLocation.x / imageView.frame.size.width)
Expand All @@ -235,31 +237,31 @@ open class FloatRatingView: UIView {
}
break
}

// Check min rating
rating = newRating < Double(minRating) ? Double(minRating) : newRating

// Update delegate
delegate?.floatRatingView?(self, isUpdating: rating)
}


// MARK: UIView

// Override to calculate ImageView frames
override open func layoutSubviews() {
super.layoutSubviews()

guard let emptyImage = emptyImage else {
return
}

let desiredImageWidth = frame.size.width / CGFloat(emptyImageViews.count)
let maxImageWidth = max(minImageSize.width, desiredImageWidth)
let maxImageHeight = max(minImageSize.height, frame.size.height)
let imageViewSize = sizeForImage(emptyImage, inSize: CGSize(width: maxImageWidth, height: maxImageHeight))
let imageXOffset = (frame.size.width - (imageViewSize.width * CGFloat(emptyImageViews.count))) /
CGFloat((emptyImageViews.count - 1))
CGFloat((emptyImageViews.count - 1))

for i in 0..<maxRating {
let imageFrame = CGRect(x: i == 0 ? 0 : CGFloat(i)*(imageXOffset+imageViewSize.width), y: 0, width: imageViewSize.width, height: imageViewSize.height)
Expand All @@ -273,17 +275,17 @@ open class FloatRatingView: UIView {

refresh()
}


// MARK: Touch events

override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
updateLocation(touch)
}

override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
Expand All @@ -301,3 +303,29 @@ open class FloatRatingView: UIView {
delegate?.floatRatingView?(self, didUpdate: rating)
}
}

// MARK: - StarRatingView+RTLSupport
public extension FloatRatingView {
/// Flip the View to support Right to Left Languages based on the view semantic
func flipViewIfNeeded() {
var direction: UIUserInterfaceLayoutDirection
if #available(iOS 10.0, *) {
direction = self.effectiveUserInterfaceLayoutDirection
} else { // Fallback on earlier versions
if #available(iOS 9.0, *) {
// The view is shown in right-to-left mode right now.
direction = UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute)
} else { // Fallback on earlier versions
direction = UIApplication.shared.userInterfaceLayoutDirection
}
}

if case .rightToLeft = direction {
flip()
}
}

private func flip() {
self.transform = CGAffineTransform(scaleX: -1.0, y: 1.0);
}
}